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
Pull Request — master (#1005)
by Dan
04:25
created

AbstractSmrPort::shootPlayer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 1
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.85; // 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' => [], TRADER_SELLS => [], TRADER_BUYS => []);
46
	protected $goodAmounts;
47
	protected $goodAmountsChanged = array();
48
	protected $goodDistances;
49
50
	protected $cachedVersion = false;
51
	protected $cachedTime;
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 = MySqlDatabase::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($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 = MySqlDatabase::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($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->cachedTime = SmrSession::getTime();
130
		$this->db = MySqlDatabase::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() {
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 (SmrSession::getTime() < $this->getReinforceTime() + self::TIME_FEDS_STAY) {
184
				$federalMod = (self::TIME_FEDS_STAY - (SmrSession::getTime() - $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($goodID, $secondsSinceLastUpdate) {
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() {
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 = SmrSession::getTime() - $this->db->getInt('last_update');
236
				$this->restockGood($goodID, $secondsSinceLastUpdate);
237
			}
238
		}
239
	}
240
241
	private function getVisibleGoods($transaction, AbstractSmrPlayer $player = null) {
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) {
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) {
264
		return $this->getVisibleGoods(TRADER_BUYS, $player);
265
	}
266
267
	public function getAllGoodIDs() {
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() {
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() {
282
		return $this->goodIDs[TRADER_BUYS];
283
	}
284
285
	public function getGood($goodID) {
286
		if ($this->hasGood($goodID)) {
287
			return Globals::getGood($goodID);
288
		} else {
289
			$return = false;
290
			return $return;
291
		}
292
	}
293
294
	public function getGoodDistance($goodID) {
295
		if (!isset($this->goodDistances[$goodID])) {
296
			$x = $this->getGood($goodID);
297
			if ($x === false) {
298
				throw new Exception('This port does not have this good!');
299
			}
300
			if ($this->hasGood($goodID, TRADER_BUYS)) {
301
				$x['TransactionType'] = TRADER_SELLS;
302
			} else {
303
				$x['TransactionType'] = TRADER_BUYS;
304
			}
305
			$di = Plotter::findDistanceToX($x, $this->getSector(), true);
306
			if (is_object($di)) {
307
				$di = $di->getRelativeDistance();
308
			}
309
			$this->goodDistances[$goodID] = max(1, $di);
310
		}
311
		return $this->goodDistances[$goodID];
312
	}
313
314
	/**
315
	 * Returns the transaction type for this good (Buy or Sell).
316
	 * Note: this is the player's transaction, not the port's.
317
	 */
318
	public function getGoodTransaction($goodID) {
319
		foreach ([TRADER_BUYS, TRADER_SELLS] as $transaction) {
320
			if ($this->hasGood($goodID, $transaction)) {
321
				return $transaction;
322
			}
323
		}
324
	}
325
326
	public function hasGood($goodID, $type = false) {
327
		if ($type === false) {
328
			$type = 'All';
329
		}
330
		return in_array($goodID, $this->goodIDs[$type]);
331
	}
332
333
	private function setGoodAmount($goodID, $amount, $doUpdate = true) {
334
		if ($this->isCachedVersion()) {
335
			throw new Exception('Cannot update a cached port!');
336
		}
337
		// The new amount must be between 0 and the max for this good
338
		$amount = max(0, min($amount, $this->getGood($goodID)['Max']));
339
		if ($this->getGoodAmount($goodID) == $amount) {
340
			return;
341
		}
342
		$this->goodAmounts[$goodID] = $amount;
343
344
		if ($doUpdate) {
345
			// This goodID will be changed in the db during `update()`
346
			$this->goodAmountsChanged[$goodID] = true;
347
		}
348
	}
349
350
	public function getGoodAmount($goodID) {
351
		return $this->goodAmounts[$goodID];
352
	}
353
354
	public function decreaseGood(array $good, $amount, $doRefresh) {
355
		$this->setGoodAmount($good['ID'], $this->getGoodAmount($good['ID']) - $amount);
356
		if ($doRefresh === true) {
357
			//get id of goods to replenish
358
			$this->refreshGoods($good['Class'], $amount);
359
		}
360
	}
361
362
	public function increaseGoodAmount($goodID, $amount) {
363
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) + $amount);
364
	}
365
366
	public function decreaseGoodAmount($goodID, $amount) {
367
		$this->setGoodAmount($goodID, $this->getGoodAmount($goodID) - $amount);
368
	}
369
370
	/**
371
	 * Adds extra stock to goods in the tier above a good that was traded
372
	 */
373
	protected function refreshGoods($classTraded, $amountTraded) {
374
		$refreshAmount = IRound($amountTraded * self::REFRESH_PER_GOOD);
375
		//refresh goods that need it
376
		$refreshClass = $classTraded + 1;
377
		foreach ($this->getAllGoodIDs() as $goodID) {
378
			$goodClass = Globals::getGood($goodID)['Class'];
379
			if ($goodClass == $refreshClass) {
380
				$this->increaseGoodAmount($goodID, $refreshAmount);
381
			}
382
		}
383
	}
384
385
	protected function tradeGoods(array $good, $goodsTraded, $exp) {
386
		$goodsTradedMoney = $goodsTraded * self::GOODS_TRADED_MONEY_MULTIPLIER;
387
		$this->increaseUpgrade($goodsTradedMoney);
388
		$this->increaseCredits($goodsTradedMoney);
389
		$this->increaseExperience($exp);
390
		$this->decreaseGood($good, $goodsTraded, true);
391
	}
392
393
	public function buyGoods(array $good, $goodsTraded, $idealPrice, $bargainPrice, $exp) {
394
		$this->tradeGoods($good, $goodsTraded, $exp);
395
		// Limit upgrade/credits to prevent massive increases in a single trade
396
		$cappedBargainPrice = min(max($idealPrice, $goodsTraded * 1000), $bargainPrice);
397
		$this->increaseUpgrade($cappedBargainPrice);
398
		$this->increaseCredits($cappedBargainPrice);
399
	}
400
401
	public function sellGoods(array $good, $goodsTraded, $exp) {
402
		$this->tradeGoods($good, $goodsTraded, $exp);
403
	}
404
405
	public function stealGoods(array $good, $goodsTraded) {
406
		$this->decreaseGood($good, $goodsTraded, false);
407
	}
408
409
	public function checkForUpgrade() {
410
		if ($this->isCachedVersion()) {
411
			throw new Exception('Cannot upgrade a cached port!');
412
		}
413
		$upgrades = 0;
414
		while ($this->upgrade >= $this->getUpgradeRequirement() && $this->level < 9) {
415
			++$upgrades;
416
			$this->decreaseUpgrade($this->getUpgradeRequirement());
417
			$this->decreaseCredits($this->getUpgradeRequirement());
418
			$this->doUpgrade();
419
		}
420
		return $upgrades;
421
	}
422
423
	/**
424
	 * This function should only be used in universe creation to set
425
	 * ports to a specific level.
426
	 */
427
	public function upgradeToLevel($level) {
428
		if ($this->isCachedVersion()) {
429
			throw new Exception('Cannot upgrade a cached port!');
430
		}
431
		while ($this->getLevel() < $level) {
432
			$this->doUpgrade();
433
		}
434
		while ($this->getLevel() > $level) {
435
			$this->doDowngrade();
436
		}
437
	}
438
439
	/**
440
	 * Returns the good class associated with the given level.
441
	 * If no level specified, will use the current port level.
442
	 * This is useful for determining what trade goods to add/remove.
443
	 */
444
	protected function getGoodClassAtLevel($level = false) {
445
		if ($level === false) {
446
			$level = $this->getLevel();
447
		}
448
		if ($level <= 2) {
449
			return 1;
450
		} elseif ($level <= 6) {
451
			return 2;
452
		} else {
453
			return 3;
454
		}
455
	}
456
457
	protected function selectAndAddGood($goodClass) {
458
		$GOODS = Globals::getGoods();
459
		shuffle($GOODS);
460
		foreach ($GOODS as $good) {
461
			if (!$this->hasGood($good['ID']) && $good['Class'] == $goodClass) {
462
				$transactionType = rand(1, 2) == 1 ? TRADER_BUYS : TRADER_SELLS;
463
				$this->addPortGood($good['ID'], $transactionType);
464
				return $good;
465
			}
466
		}
467
		throw new Exception('Failed to add a good!');
468
	}
469
470
	protected function doUpgrade() {
471
		if ($this->isCachedVersion()) {
472
			throw new Exception('Cannot upgrade a cached port!');
473
		}
474
475
		$this->increaseLevel(1);
476
		$goodClass = $this->getGoodClassAtLevel();
477
		$this->selectAndAddGood($goodClass);
478
479
		if ($this->getLevel() == 1) {
480
			// Add 2 extra goods when upgrading to level 1 (i.e. in Uni Gen)
481
			$this->selectAndAddGood($goodClass);
482
			$this->selectAndAddGood($goodClass);
483
		}
484
	}
485
486
	public function getUpgradeRequirement() {
487
//		return round(exp($this->getLevel()/1.7)+3)*1000000;
488
		return $this->getLevel() * 1000000;
489
	}
490
491
	/**
492
	 * Manually set port goods.
493
	 * Input must be an array of good_id => transaction.
494
	 * Only modifies goods that need to change.
495
	 * Returns false on invalid input.
496
	 */
497
	public function setPortGoods(array $goods) {
498
		// Validate the input list of goods to make sure we have the correct
499
		// number of each good class for this port level.
500
		$givenClasses = [];
501
		foreach (array_keys($goods) as $goodID) {
502
			$givenClasses[] = Globals::getGood($goodID)['Class'];
503
		}
504
		$expectedClasses = [1, 1]; // Level 1 has 2 extra Class 1 goods
505
		foreach (range(1, $this->getLevel()) as $level) {
506
			$expectedClasses[] = $this->getGoodClassAtLevel($level);
507
		}
508
		if ($givenClasses != $expectedClasses) {
509
			return false;
510
		}
511
512
		// Remove goods not specified or that have the wrong transaction
513
		foreach ($this->getAllGoodIDs() as $goodID) {
514
			if (!isset($goods[$goodID]) || !$this->hasGood($goodID, $goods[$goodID])) {
515
				$this->removePortGood($goodID);
516
			}
517
		}
518
		// Add goods
519
		foreach ($goods as $goodID => $trans) {
520
			$this->addPortGood($goodID, $trans);
521
		}
522
		return true;
523
	}
524
525
	/**
526
	 * Add good with given ID to the port, with transaction $type
527
	 * as either "Buy" or "Sell", meaning the player buys or sells.
528
	 * If the port already has this transaction, do nothing.
529
	 *
530
	 * NOTE: make sure to adjust the port level appropriately if
531
	 * calling this function directly.
532
	 */
533
	public function addPortGood($goodID, $type) {
534
		if ($this->isCachedVersion()) {
535
			throw new Exception('Cannot update a cached port!');
536
		}
537
		if ($this->hasGood($goodID, $type)) {
538
			return;
539
		}
540
541
		$this->goodIDs['All'][] = $goodID;
542
		$this->goodIDs[$type][] = $goodID;
543
		// sort ID arrays, since the good ID might not be the largest
544
		sort($this->goodIDs['All']);
545
		sort($this->goodIDs[$type]);
546
547
		$this->goodAmounts[$goodID] = Globals::getGood($goodID)['Max'];
548
		$this->cacheIsValid = false;
549
		$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(SmrSession::getTime()) . ')');
550
		$this->db->query('DELETE FROM route_cache WHERE game_id=' . $this->db->escapeNumber($this->getGameID()));
551
	}
552
553
	/**
554
	 * Remove good with given ID from the port.
555
	 * If the port does not have this good, do nothing.
556
	 *
557
	 * NOTE: make sure to adjust the port level appropriately if
558
	 * calling this function directly.
559
	 */
560
	public function removePortGood($goodID) {
561
		if ($this->isCachedVersion()) {
562
			throw new Exception('Cannot update a cached port!');
563
		}
564
		if (!$this->hasGood($goodID)) {
565
			return;
566
		}
567
		if (($key = array_search($goodID, $this->goodIDs['All'])) !== false) {
568
			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

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