Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Failed Conditions
Push — main ( 30fe2e...c58695 )
by Dan
04:45
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 47
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 35
c 0
b 0
f 0
nc 8
nop 8
dl 0
loc 47
rs 8.4266

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc.php');
3
4
use Smr\BountyType;
5
use Smr\Database;
6
use Smr\DatabaseRecord;
7
use Smr\DisplayNameValidator;
8
use Smr\Epoch;
9
use Smr\Exceptions\AccountNotFound;
10
use Smr\Exceptions\PlayerNotFound;
11
use Smr\Exceptions\UserError;
12
use Smr\Messages;
13
use Smr\Pages\Player\ExamineTrader;
14
use Smr\Pages\Player\NewbieLeaveProcessor;
15
use Smr\Pages\Player\Planet\KickProcessor;
16
use Smr\Pages\Player\SearchForTraderResult;
17
use Smr\Pages\Player\WeaponDisplayToggleProcessor;
18
use Smr\Path;
19
use Smr\Race;
20
use Smr\ScoutMessageGroupType;
21
use Smr\StoredDestination;
22
use Smr\TurnsLevel;
23
24
abstract class AbstractSmrPlayer {
25
26
	use Traits\RaceID;
27
28
	protected const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
29
	protected const TIME_FOR_ALLIANCE_SWITCH = 0;
30
31
	protected const SHIP_INSURANCE_FRACTION = 0.25; // ship value regained on death
32
33
	protected const HOF_CHANGED = 1;
34
	protected const HOF_NEW = 2;
35
36
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
37
	protected static array $CACHE_SECTOR_PLAYERS = [];
38
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
39
	protected static array $CACHE_PLANET_PLAYERS = [];
40
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
41
	protected static array $CACHE_ALLIANCE_PLAYERS = [];
42
	/** @var array<int, array<int, SmrPlayer>> */
43
	protected static array $CACHE_PLAYERS = [];
44
45
	protected Database $db;
46
	protected readonly string $SQL;
47
48
	protected string $playerName;
49
	protected int $playerID;
50
	protected int $sectorID;
51
	protected int $lastSectorID;
52
	protected int $newbieTurns;
53
	protected bool $dead;
54
	protected bool $npc = false; // initialized for legacy combat logs
55
	protected bool $newbieStatus;
56
	protected bool $newbieWarning;
57
	protected bool $landedOnPlanet;
58
	protected int $lastActive;
59
	protected int $credits;
60
	protected int $alignment;
61
	protected int $experience;
62
	protected ?int $level;
63
	protected int $allianceID;
64
	protected int $shipID;
65
	protected int $kills;
66
	protected int $deaths;
67
	protected int $assists;
68
	/** @var array<int, int> */
69
	protected array $personalRelations;
70
	/** @var array<int, int> */
71
	protected array $relations;
72
	protected int $militaryPayment;
73
	/** @var array<int, array<string, mixed>> */
74
	protected array $bounties;
75
	protected int $turns;
76
	protected int $lastCPLAction;
77
	/** @var array<int, array<string, mixed>> */
78
	protected array $missions;
79
80
	/** @var array<string, array<string, mixed>> */
81
	protected array $tickers;
82
	protected int $lastTurnUpdate;
83
	protected int $lastNewsUpdate;
84
	protected string $attackColour;
85
	protected int $allianceJoinable;
86
	protected int $lastPort;
87
	protected int $bank;
88
	protected int $zoom;
89
	protected bool $displayMissions;
90
	protected bool $displayWeapons;
91
	protected bool $forceDropMessages;
92
	protected ScoutMessageGroupType $scoutMessageGroupType;
93
	protected bool $ignoreGlobals;
94
	protected ?Path $plottedCourse;
95
	protected bool $nameChanged;
96
	protected bool $raceChanged;
97
	protected bool $combatDronesKamikazeOnMines;
98
	protected string|false $customShipName;
99
	/** @var array<int, StoredDestination> */
100
	protected array $storedDestinations;
101
	/** @var array<int, bool> */
102
	protected array $canFed;
103
	protected bool $underAttack;
104
105
	/** @var array<int> */
106
	protected array $unvisitedSectors;
107
	/** @var array<int, int> */
108
	protected array $allianceRoles = [
109
		0 => 0,
110
	];
111
112
	protected bool $draftLeader;
113
	protected string|false $gpWriter;
114
	/** @var array<string, float> */
115
	protected array $HOF;
116
	/** @var array<string, string> */
117
	protected static array $HOFVis;
118
119
	protected bool $hasChanged = false;
120
	/** @var array<string, int> */
121
	protected array $hasHOFChanged = [];
122
	/** @var array<string, int> */
123
	protected static array $hasHOFVisChanged = [];
124
	/** @var array<int, bool> */
125
	protected array $hasBountyChanged = [];
126
127
	public static function clearCache(): void {
128
		self::$CACHE_PLAYERS = [];
129
		self::$CACHE_SECTOR_PLAYERS = [];
130
		self::$CACHE_PLANET_PLAYERS = [];
131
		self::$CACHE_ALLIANCE_PLAYERS = [];
132
	}
133
134
	public static function savePlayers(): void {
135
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
136
			foreach ($gamePlayers as $player) {
137
				$player->save();
138
			}
139
		}
140
	}
141
142
	/**
143
	 * @param array<int> $allianceIDs
144
	 * @return array<int, SmrPlayer>
145
	 */
146
	public static function getSectorPlayersByAlliances(int $gameID, int $sectorID, array $allianceIDs, bool $forceUpdate = false): array {
147
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
148
		foreach ($players as $accountID => $player) {
149
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
150
				unset($players[$accountID]);
151
			}
152
		}
153
		return $players;
154
	}
155
156
	/**
157
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
158
	 * but for an entire galaxy rather than a single sector. This is useful
159
	 * for reducing the number of queries in galaxy-wide processing.
160
	 *
161
	 * @return array<int, array<int, SmrPlayer>>
162
	 */
163
	public static function getGalaxyPlayers(int $gameID, int $galaxyID, bool $forceUpdate = false): array {
164
		$db = Database::getInstance();
165
		$dbResult = $db->read('SELECT player.* FROM player LEFT JOIN sector USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_HIDDEN) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
166
		$galaxyPlayers = [];
167
		foreach ($dbResult->records() as $dbRecord) {
168
			$sectorID = $dbRecord->getInt('sector_id');
169
			$accountID = $dbRecord->getInt('account_id');
170
			$player = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
171
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
172
			$galaxyPlayers[$sectorID][$accountID] = $player;
173
		}
174
		return $galaxyPlayers;
175
	}
176
177
	/**
178
	 * @return array<int, SmrPlayer>
179
	 */
180
	public static function getSectorPlayers(int $gameID, int $sectorID, bool $forceUpdate = false): array {
181
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
182
			$db = Database::getInstance();
183
			$dbResult = $db->read('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_HIDDEN) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
184
			$players = [];
185
			foreach ($dbResult->records() as $dbRecord) {
186
				$accountID = $dbRecord->getInt('account_id');
187
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
188
			}
189
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
190
		}
191
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
192
	}
193
194
	/**
195
	 * @return array<int, SmrPlayer>
196
	 */
197
	public static function getPlanetPlayers(int $gameID, int $sectorID, bool $forceUpdate = false): array {
198
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
199
			$db = Database::getInstance();
200
			$dbResult = $db->read('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(true) . ' AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
201
			$players = [];
202
			foreach ($dbResult->records() as $dbRecord) {
203
				$accountID = $dbRecord->getInt('account_id');
204
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
205
			}
206
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
207
		}
208
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
209
	}
210
211
	/**
212
	 * @return array<int, SmrPlayer>
213
	 */
214
	public static function getAlliancePlayers(int $gameID, int $allianceID, bool $forceUpdate = false): array {
215
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
216
			$db = Database::getInstance();
217
			$dbResult = $db->read('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
218
			$players = [];
219
			foreach ($dbResult->records() as $dbRecord) {
220
				$accountID = $dbRecord->getInt('account_id');
221
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
222
			}
223
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
224
		}
225
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
226
	}
227
228
	public static function getPlayer(int $accountID, int $gameID, bool $forceUpdate = false, DatabaseRecord $dbRecord = null): SmrPlayer {
229
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
230
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $dbRecord);
231
		}
232
		return self::$CACHE_PLAYERS[$gameID][$accountID];
233
	}
234
235
	public static function getPlayerByPlayerID(int $playerID, int $gameID, bool $forceUpdate = false): SmrPlayer {
236
		$db = Database::getInstance();
237
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID));
238
		if ($dbResult->hasRecord()) {
239
			$dbRecord = $dbResult->record();
240
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
241
		}
242
		throw new PlayerNotFound('Player ID not found.');
243
	}
244
245
	public static function getPlayerByPlayerName(string $playerName, int $gameID, bool $forceUpdate = false): SmrPlayer {
246
		$db = Database::getInstance();
247
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName));
248
		if ($dbResult->hasRecord()) {
249
			$dbRecord = $dbResult->record();
250
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
251
		}
252
		throw new PlayerNotFound('Player Name not found.');
253
	}
254
255
	protected function __construct(
256
		protected readonly int $gameID,
257
		protected readonly int $accountID,
258
		DatabaseRecord $dbRecord = null
259
	) {
260
		$this->db = Database::getInstance();
261
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
0 ignored issues
show
Bug introduced by
The property SQL is declared read-only in AbstractSmrPlayer.
Loading history...
262
263
		if ($dbRecord === null) {
264
			$dbResult = $this->db->read('SELECT * FROM player WHERE ' . $this->SQL);
265
			if ($dbResult->hasRecord()) {
266
				$dbRecord = $dbResult->record();
267
			}
268
		}
269
		if ($dbRecord === null) {
270
			throw new PlayerNotFound('Invalid accountID: ' . $accountID . ' OR gameID: ' . $gameID);
271
		}
272
273
		$this->playerName = $dbRecord->getString('player_name');
274
		$this->playerID = $dbRecord->getInt('player_id');
275
		$this->sectorID = $dbRecord->getInt('sector_id');
276
		$this->lastSectorID = $dbRecord->getInt('last_sector_id');
277
		$this->turns = $dbRecord->getInt('turns');
278
		$this->lastTurnUpdate = $dbRecord->getInt('last_turn_update');
279
		$this->newbieTurns = $dbRecord->getInt('newbie_turns');
280
		$this->lastNewsUpdate = $dbRecord->getInt('last_news_update');
281
		$this->attackColour = $dbRecord->getString('attack_warning');
282
		$this->dead = $dbRecord->getBoolean('dead');
283
		$this->npc = $dbRecord->getBoolean('npc');
284
		$this->newbieStatus = $dbRecord->getBoolean('newbie_status');
285
		$this->landedOnPlanet = $dbRecord->getBoolean('land_on_planet');
286
		$this->lastActive = $dbRecord->getInt('last_active');
287
		$this->lastCPLAction = $dbRecord->getInt('last_cpl_action');
288
		$this->raceID = $dbRecord->getInt('race_id');
289
		$this->credits = $dbRecord->getInt('credits');
290
		$this->experience = $dbRecord->getInt('experience');
291
		$this->alignment = $dbRecord->getInt('alignment');
292
		$this->militaryPayment = $dbRecord->getInt('military_payment');
293
		$this->allianceID = $dbRecord->getInt('alliance_id');
294
		$this->allianceJoinable = $dbRecord->getInt('alliance_join');
295
		$this->shipID = $dbRecord->getInt('ship_type_id');
296
		$this->kills = $dbRecord->getInt('kills');
297
		$this->deaths = $dbRecord->getInt('deaths');
298
		$this->assists = $dbRecord->getInt('assists');
299
		$this->lastPort = $dbRecord->getInt('last_port');
300
		$this->bank = $dbRecord->getInt('bank');
301
		$this->zoom = $dbRecord->getInt('zoom');
302
		$this->displayMissions = $dbRecord->getBoolean('display_missions');
303
		$this->displayWeapons = $dbRecord->getBoolean('display_weapons');
304
		$this->forceDropMessages = $dbRecord->getBoolean('force_drop_messages');
305
		$this->scoutMessageGroupType = ScoutMessageGroupType::from($dbRecord->getString('group_scout_messages'));
306
		$this->ignoreGlobals = $dbRecord->getBoolean('ignore_globals');
307
		$this->newbieWarning = $dbRecord->getBoolean('newbie_warning');
308
		$this->nameChanged = $dbRecord->getBoolean('name_changed');
309
		$this->raceChanged = $dbRecord->getBoolean('race_changed');
310
		$this->combatDronesKamikazeOnMines = $dbRecord->getBoolean('combat_drones_kamikaze_on_mines');
311
		$this->underAttack = $dbRecord->getBoolean('under_attack');
312
	}
313
314
	/**
315
	 * Insert a new player into the database. Returns the new player object.
316
	 */
317
	public static function createPlayer(int $accountID, int $gameID, string $playerName, int $raceID, bool $isNewbie, bool $npc = false): self {
318
		$time = Epoch::time();
319
		$db = Database::getInstance();
320
		$db->lockTable('player');
321
322
		// Player names must be unique within each game
323
		try {
324
			self::getPlayerByPlayerName($playerName, $gameID);
325
			$db->unlock();
326
			throw new UserError('That player name already exists.');
327
		} catch (PlayerNotFound) {
328
			// Player name does not yet exist, we may proceed
329
		}
330
331
		// Get the next available player ID (start at 1 if no players yet)
332
		$dbResult = $db->read('SELECT IFNULL(MAX(player_id), 0) AS player_id FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
333
		$playerID = $dbResult->record()->getInt('player_id') + 1;
334
335
		$startSectorID = 0; // Temporarily put player into non-existent sector
336
		$db->insert('player', [
337
			'account_id' => $db->escapeNumber($accountID),
338
			'game_id' => $db->escapeNumber($gameID),
339
			'player_id' => $db->escapeNumber($playerID),
340
			'player_name' => $db->escapeString($playerName),
341
			'race_id' => $db->escapeNumber($raceID),
342
			'sector_id' => $db->escapeNumber($startSectorID),
343
			'last_cpl_action' => $db->escapeNumber($time),
344
			'last_active' => $db->escapeNumber($time),
345
			'npc' => $db->escapeBoolean($npc),
346
			'newbie_status' => $db->escapeBoolean($isNewbie),
347
		]);
348
		$db->unlock();
349
350
		$player = self::getPlayer($accountID, $gameID);
351
		$player->setSectorID($player->getHome());
352
		return $player;
353
	}
354
355
	/**
356
	 * Get array of players whose info can be accessed by this player.
357
	 * Skips players who are not in the same alliance as this player.
358
	 *
359
	 * @return array<AbstractSmrPlayer>
360
	 */
361
	public function getSharingPlayers(bool $forceUpdate = false): array {
362
		$results = [$this];
363
364
		// Only return this player if not in an alliance
365
		if (!$this->hasAlliance()) {
366
			return $results;
367
		}
368
369
		// Get other players who are sharing info for this game.
370
		// NOTE: game_id=0 means that player shares info for all games.
371
		$dbResult = $this->db->read('SELECT from_account_id FROM account_shares_info WHERE to_account_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND (game_id=0 OR game_id=' . $this->db->escapeNumber($this->getGameID()) . ')');
372
		foreach ($dbResult->records() as $dbRecord) {
373
			try {
374
				$otherPlayer = self::getPlayer($dbRecord->getInt('from_account_id'), $this->getGameID(), $forceUpdate);
375
			} catch (PlayerNotFound) {
376
				// Skip players that have not joined this game
377
				continue;
378
			}
379
380
			// players must be in the same alliance
381
			if ($this->sameAlliance($otherPlayer)) {
382
				$results[] = $otherPlayer;
383
			}
384
		}
385
		return $results;
386
	}
387
388
	public function getSQL(): string {
389
		return $this->SQL;
390
	}
391
392
	public function getZoom(): int {
393
		return $this->zoom;
394
	}
395
396
	protected function setZoom(int $zoom): void {
397
		// Set the zoom level between [1, 9]
398
		$zoom = max(1, min(9, $zoom));
399
		if ($this->zoom == $zoom) {
400
			return;
401
		}
402
		$this->zoom = $zoom;
403
		$this->hasChanged = true;
404
	}
405
406
	public function increaseZoom(int $zoom): void {
407
		if ($zoom < 0) {
408
			throw new Exception('Trying to increase negative zoom.');
409
		}
410
		$this->setZoom($this->getZoom() + $zoom);
411
	}
412
413
	public function decreaseZoom(int $zoom): void {
414
		if ($zoom < 0) {
415
			throw new Exception('Trying to decrease negative zoom.');
416
		}
417
		$this->setZoom($this->getZoom() - $zoom);
418
	}
419
420
	public function getAttackColour(): string {
421
		return $this->attackColour;
422
	}
423
424
	public function setAttackColour(string $colour): void {
425
		if ($this->attackColour == $colour) {
426
			return;
427
		}
428
		$this->attackColour = $colour;
429
		$this->hasChanged = true;
430
	}
431
432
	public function isIgnoreGlobals(): bool {
433
		return $this->ignoreGlobals;
434
	}
435
436
	public function setIgnoreGlobals(bool $bool): void {
437
		if ($this->ignoreGlobals == $bool) {
438
			return;
439
		}
440
		$this->ignoreGlobals = $bool;
441
		$this->hasChanged = true;
442
	}
443
444
	public function getAccount(): SmrAccount {
445
		return SmrAccount::getAccount($this->getAccountID());
446
	}
447
448
	public function getAccountID(): int {
449
		return $this->accountID;
450
	}
451
452
	public function getGameID(): int {
453
		return $this->gameID;
454
	}
455
456
	public function getGame(bool $forceUpdate = false): SmrGame {
457
		return SmrGame::getGame($this->gameID, $forceUpdate);
458
	}
459
460
	public function getNewbieTurns(): int {
461
		return $this->newbieTurns;
462
	}
463
464
	public function hasNewbieTurns(): bool {
465
		return $this->getNewbieTurns() > 0;
466
	}
467
468
	public function setNewbieTurns(int $newbieTurns): void {
469
		if ($this->newbieTurns == $newbieTurns) {
470
			return;
471
		}
472
		$this->newbieTurns = $newbieTurns;
473
		$this->hasChanged = true;
474
	}
475
476
	public function getShip(bool $forceUpdate = false): AbstractSmrShip {
477
		return SmrShip::getShip($this, $forceUpdate);
478
	}
479
480
	public function getShipTypeID(): int {
481
		return $this->shipID;
482
	}
483
484
	/**
485
	 * Do not call directly. Use SmrShip::setTypeID instead.
486
	 */
487
	public function setShipTypeID(int $shipID): void {
488
		if ($this->shipID == $shipID) {
489
			return;
490
		}
491
		$this->shipID = $shipID;
492
		$this->hasChanged = true;
493
	}
494
495
	public function hasCustomShipName(): bool {
496
		return $this->getCustomShipName() !== false;
497
	}
498
499
	public function getCustomShipName(): string|false {
500
		if (!isset($this->customShipName)) {
501
			$dbResult = $this->db->read('SELECT * FROM ship_has_name WHERE ' . $this->SQL);
502
			if ($dbResult->hasRecord()) {
503
				$this->customShipName = $dbResult->record()->getString('ship_name');
504
			} else {
505
				$this->customShipName = false;
506
			}
507
		}
508
		return $this->customShipName;
509
	}
510
511
	public function setCustomShipName(string $name): void {
512
		$this->db->replace('ship_has_name', [
513
			'game_id' => $this->db->escapeNumber($this->getGameID()),
514
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
515
			'ship_name' => $this->db->escapeString($name),
516
		]);
517
	}
518
519
	/**
520
	 * Get planet owned by this player.
521
	 * Returns null if this player does not own a planet.
522
	 */
523
	public function getPlanet(): ?SmrPlanet {
524
		$dbResult = $this->db->read('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
525
		if ($dbResult->hasRecord()) {
526
			$dbRecord = $dbResult->record();
527
			return SmrPlanet::getPlanet($this->getGameID(), $dbRecord->getInt('sector_id'), false, $dbRecord);
528
		}
529
		return null;
530
	}
531
532
	public function getSectorPlanet(): SmrPlanet {
533
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
534
	}
535
536
	public function getSectorPort(): AbstractSmrPort {
537
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
538
	}
539
540
	public function getSectorID(): int {
541
		return $this->sectorID;
542
	}
543
544
	public function getSector(): SmrSector {
545
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
546
	}
547
548
	public function setSectorID(int $sectorID): void {
549
		if ($this->sectorID == $sectorID) {
550
			return;
551
		}
552
553
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
554
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
555
556
		$this->setLastSectorID($this->getSectorID());
557
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
558
		$this->sectorID = $sectorID;
559
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
560
		$this->hasChanged = true;
561
562
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
563
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
564
	}
565
566
	public function getLastSectorID(): int {
567
		return $this->lastSectorID;
568
	}
569
570
	public function setLastSectorID(int $lastSectorID): void {
571
		if ($this->lastSectorID == $lastSectorID) {
572
			return;
573
		}
574
		$this->lastSectorID = $lastSectorID;
575
		$this->hasChanged = true;
576
	}
577
578
	public function getHome(): int {
579
		// Draft games may have customized home sectors
580
		if ($this->getGame()->isGameType(SmrGame::GAME_TYPE_DRAFT) && $this->hasAlliance()) {
581
			$leaderID = $this->getAlliance()->getLeaderID();
582
			$dbResult = $this->db->read('SELECT home_sector_id FROM draft_leaders WHERE account_id = ' . $this->db->escapeNumber($leaderID) . ' AND game_id = ' . $this->db->escapeNumber($this->getGameID()));
583
			if ($dbResult->hasRecord()) {
584
				return $dbResult->record()->getInt('home_sector_id');
585
			}
586
		}
587
588
		// get his home sector
589
		$hq_id = GOVERNMENT + $this->getRaceID();
590
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
591
		if (empty($raceHqSectors)) {
592
			// No HQ, default to sector 1
593
			return 1;
594
		}
595
		// If race has multiple HQ's for some reason, use the first one
596
		return key($raceHqSectors);
597
	}
598
599
	public function isDead(): bool {
600
		return $this->dead;
601
	}
602
603
	public function isNPC(): bool {
604
		return $this->npc;
605
	}
606
607
	/**
608
	 * Does the player have Newbie status?
609
	 */
610
	public function hasNewbieStatus(): bool {
611
		return $this->newbieStatus;
612
	}
613
614
	/**
615
	 * Update the player's newbie status if it has changed.
616
	 * This function queries the account, so use sparingly.
617
	 */
618
	public function updateNewbieStatus(): void {
619
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
620
		if ($this->newbieStatus != $accountNewbieStatus) {
621
			$this->newbieStatus = $accountNewbieStatus;
622
			$this->hasChanged = true;
623
		}
624
	}
625
626
	/**
627
	 * Has this player been designated as the alliance flagship?
628
	 */
629
	public function isFlagship(): bool {
630
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
631
	}
632
633
	public function isPresident(): bool {
634
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
635
	}
636
637
	public function isOnCouncil(): bool {
638
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
639
	}
640
641
	public function isDraftLeader(): bool {
642
		if (!isset($this->draftLeader)) {
643
			$dbResult = $this->db->read('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL);
644
			$this->draftLeader = $dbResult->hasRecord();
645
		}
646
		return $this->draftLeader;
647
	}
648
649
	public function getGPWriter(): string|false {
650
		if (!isset($this->gpWriter)) {
651
			$this->gpWriter = false;
652
			$dbResult = $this->db->read('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
653
			if ($dbResult->hasRecord()) {
654
				$this->gpWriter = $dbResult->record()->getString('position');
655
			}
656
		}
657
		return $this->gpWriter;
658
	}
659
660
	public function isGPEditor(): bool {
661
		return $this->getGPWriter() == 'editor';
662
	}
663
664
	public function isForceDropMessages(): bool {
665
		return $this->forceDropMessages;
666
	}
667
668
	public function setForceDropMessages(bool $bool): void {
669
		if ($this->forceDropMessages == $bool) {
670
			return;
671
		}
672
		$this->forceDropMessages = $bool;
673
		$this->hasChanged = true;
674
	}
675
676
	public function getScoutMessageGroupLimit(): int {
677
		return match ($this->scoutMessageGroupType) {
678
			ScoutMessageGroupType::Always => 0,
679
			ScoutMessageGroupType::Auto => MESSAGES_PER_PAGE,
680
			ScoutMessageGroupType::Never => PHP_INT_MAX,
681
		};
682
	}
683
684
	public function getScoutMessageGroupType(): ScoutMessageGroupType {
685
		return $this->scoutMessageGroupType;
686
	}
687
688
	public function setScoutMessageGroupType(ScoutMessageGroupType $setting): void {
689
		if ($this->scoutMessageGroupType === $setting) {
690
			return;
691
		}
692
		$this->scoutMessageGroupType = $setting;
693
		$this->hasChanged = true;
694
	}
695
696
	/**
697
	 * @return int Message ID
698
	 */
699
	protected static function doMessageSending(int $senderID, int $receiverID, int $gameID, int $messageTypeID, string $message, int $expires, bool $senderDelete = false, bool $unread = true): int {
700
		$message = trim($message);
701
		$db = Database::getInstance();
702
		// Keep track of the message_id so it can be returned
703
		$insertID = $db->insert('message', [
704
			'account_id' => $db->escapeNumber($receiverID),
705
			'game_id' => $db->escapeNumber($gameID),
706
			'message_type_id' => $db->escapeNumber($messageTypeID),
707
			'message_text' => $db->escapeString($message),
708
			'sender_id' => $db->escapeNumber($senderID),
709
			'send_time' => $db->escapeNumber(Epoch::time()),
710
			'expire_time' => $db->escapeNumber($expires),
711
			'sender_delete' => $db->escapeBoolean($senderDelete),
712
		]);
713
714
		if ($unread === true) {
715
			// give him the message icon
716
			$db->replace('player_has_unread_messages', [
717
				'game_id' => $db->escapeNumber($gameID),
718
				'account_id' => $db->escapeNumber($receiverID),
719
				'message_type_id' => $db->escapeNumber($messageTypeID),
720
			]);
721
		}
722
723
		switch ($messageTypeID) {
724
			case MSG_PLAYER:
725
				$receiverAccount = SmrAccount::getAccount($receiverID);
726
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isActive()) {
727
					$sender = Messages::getMessagePlayer($senderID, $gameID, $messageTypeID);
728
					if ($sender instanceof self) {
0 ignored issues
show
introduced by
$sender is never a sub-type of self.
Loading history...
729
						$sender = $sender->getDisplayName();
730
					}
731
					$mail = setupMailer();
0 ignored issues
show
Bug introduced by
The function setupMailer was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

731
					$mail = /** @scrutinizer ignore-call */ setupMailer();
Loading history...
732
					$mail->Subject = 'Message Notification';
733
					$mail->setFrom('[email protected]', 'SMR Notifications');
734
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getDateTimeFormat(), Epoch::time()) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, $gameID, true);
735
					$mail->msgHTML($bbifiedMessage);
736
					$mail->AltBody = strip_tags($bbifiedMessage);
737
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
738
					$mail->send();
739
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
740
					$receiverAccount->update();
741
				}
742
				break;
743
		}
744
745
		return $insertID;
746
	}
747
748
	public function sendMessageToBox(int $boxTypeID, string $message): void {
749
		// send him the message
750
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
751
	}
752
753
	public function sendGlobalMessage(string $message, bool $canBeIgnored = true): void {
754
		if ($canBeIgnored) {
755
			if ($this->getAccount()->isMailBanned()) {
756
				throw new UserError('You are currently banned from sending messages');
757
			}
758
		}
759
		$this->sendMessageToBox(BOX_GLOBALS, $message);
760
761
		// send to all online player
762
		$db = Database::getInstance();
763
		$dbResult = $db->read('SELECT account_id
764
					FROM active_session
765
					JOIN player USING (game_id, account_id)
766
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_INACTIVE) . '
767
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
768
						AND ignore_globals = \'FALSE\'
769
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
770
771
		foreach ($dbResult->records() as $dbRecord) {
772
			$this->sendMessage($dbRecord->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
773
		}
774
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
775
	}
776
777
	/**
778
	 * @return ($canBeIgnored is true ? int|false : int) Message ID
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($canBeIgnored at position 1 could not be parsed: Unknown type name '$canBeIgnored' at position 1 in ($canBeIgnored.
Loading history...
779
	 */
780
	public function sendMessage(int $receiverID, int $messageTypeID, string $message, bool $canBeIgnored = true, bool $unread = true, int $expires = null, bool $senderDelete = false): int|false {
781
		//get expire time
782
		if ($canBeIgnored) {
783
			if ($this->getAccount()->isMailBanned()) {
784
				throw new UserError('You are currently banned from sending messages');
785
			}
786
			// Don't send messages to players ignoring us
787
			$dbResult = $this->db->read('SELECT 1 FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
788
			if ($dbResult->hasRecord()) {
789
				return false;
790
			}
791
		}
792
793
		$message = word_filter($message);
794
795
		// If expires not specified, use default based on message type
796
		if ($expires === null) {
797
			$expires = match ($messageTypeID) {
798
				MSG_GLOBAL => 3600, // 1h
799
				MSG_PLAYER => 86400 * 31, // 1 month
800
				MSG_PLANET => 86400 * 7, // 1 week
801
				MSG_SCOUT => 86400 * 3, // 3 days
802
				MSG_POLITICAL => 86400 * 31, // 1 month
803
				MSG_ALLIANCE => 86400 * 31, // 1 month
804
				MSG_ADMIN => 86400 * 365, // 1 year
805
				MSG_CASINO => 86400 * 31, // 1 month
806
				default => 86400 * 7, // 1 week
807
			};
808
			$expires += Epoch::time();
809
		}
810
811
		// Do not put scout messages in the sender's sent box
812
		if ($messageTypeID == MSG_SCOUT) {
813
			$senderDelete = true;
814
		}
815
816
		// send him the message and return the message_id
817
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
818
	}
819
820
	public function sendMessageFromOpAnnounce(int $receiverID, string $message, int $expires = null): void {
821
		// get expire time if not set
822
		if ($expires === null) {
823
			$expires = Epoch::time() + 86400 * 14;
824
		}
825
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
826
	}
827
828
	public function sendMessageFromAllianceCommand(int $receiverID, string $message): void {
829
		$expires = Epoch::time() + 86400 * 365;
830
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
831
	}
832
833
	public static function sendMessageFromPlanet(int $gameID, int $receiverID, string $message): void {
834
		//get expire time
835
		$expires = Epoch::time() + 86400 * 31;
836
		// send him the message
837
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
838
	}
839
840
	public static function sendMessageFromPort(int $gameID, int $receiverID, string $message): void {
841
		//get expire time
842
		$expires = Epoch::time() + 86400 * 31;
843
		// send him the message
844
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
845
	}
846
847
	public static function sendMessageFromFedClerk(int $gameID, int $receiverID, string $message): void {
848
		$expires = Epoch::time() + 86400 * 365;
849
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
850
	}
851
852
	public static function sendMessageFromAdmin(int $gameID, int $receiverID, string $message, int $expires = null): void {
853
		//get expire time
854
		if ($expires === null) {
855
			$expires = Epoch::time() + 86400 * 365;
856
		}
857
		// send him the message
858
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
859
	}
860
861
	public static function sendMessageFromAllianceAmbassador(int $gameID, int $receiverID, string $message, int $expires = null): void {
862
		//get expire time
863
		if ($expires === null) {
864
			$expires = Epoch::time() + 86400 * 31;
865
		}
866
		// send him the message
867
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
868
	}
869
870
	public static function sendMessageFromCasino(int $gameID, int $receiverID, string $message, int $expires = null): void {
871
		//get expire time
872
		if ($expires === null) {
873
			$expires = Epoch::time() + 86400 * 7;
874
		}
875
		// send him the message
876
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
877
	}
878
879
	public static function sendMessageFromRace(int $raceID, int $gameID, int $receiverID, string $message, int $expires = null): void {
880
		//get expire time
881
		if ($expires === null) {
882
			$expires = Epoch::time() + 86400 * 5;
883
		}
884
		// send him the message
885
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
886
	}
887
888
	public function setMessagesRead(int $messageTypeID): void {
889
		$this->db->write('DELETE FROM player_has_unread_messages
890
							WHERE ' . $this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
891
		$this->db->write('UPDATE message SET msg_read = ' . $this->db->escapeBoolean(true) . '
892
				WHERE message_type_id = ' . $this->db->escapeNumber($messageTypeID) . ' AND ' . $this->SQL);
893
	}
894
895
	public function getSafeAttackRating(): int {
896
		return max(0, min(8, IFloor($this->getAlignment() / 150) + 4));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

896
		return max(0, min(8, /** @scrutinizer ignore-call */ IFloor($this->getAlignment() / 150) + 4));
Loading history...
897
	}
898
899
	public function hasFederalProtection(): bool {
900
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
901
		if (!$sector->offersFederalProtection()) {
902
			return false;
903
		}
904
905
		$ship = $this->getShip();
906
		if ($ship->hasIllegalGoods()) {
907
			return false;
908
		}
909
910
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
911
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
912
				if ($this->canBeProtectedByRace($fedRaceID)) {
913
					return true;
914
				}
915
			}
916
		}
917
918
		return false;
919
	}
920
921
	public function canBeProtectedByRace(int $raceID): bool {
922
		if (!isset($this->canFed)) {
923
			$this->canFed = [];
924
			foreach (Race::getAllIDs() as $raceID2) {
925
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
926
			}
927
			$dbResult = $this->db->read('SELECT race_id, allowed FROM player_can_fed
928
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(Epoch::time()));
929
			foreach ($dbResult->records() as $dbRecord) {
930
				$this->canFed[$dbRecord->getInt('race_id')] = $dbRecord->getBoolean('allowed');
931
			}
932
		}
933
		return $this->canFed[$raceID];
934
	}
935
936
	/**
937
	 * Returns a boolean identifying if the player can currently
938
	 * participate in battles.
939
	 */
940
	public function canFight(): bool {
941
		return !($this->hasNewbieTurns() ||
942
		         $this->isDead() ||
943
		         $this->isLandedOnPlanet() ||
944
		         $this->hasFederalProtection());
945
	}
946
947
	public function setDead(bool $bool): void {
948
		if ($this->dead == $bool) {
949
			return;
950
		}
951
		$this->dead = $bool;
952
		$this->hasChanged = true;
953
	}
954
955
	public function getKills(): int {
956
		return $this->kills;
957
	}
958
959
	public function increaseKills(int $kills): void {
960
		if ($kills < 0) {
961
			throw new Exception('Trying to increase negative kills.');
962
		}
963
		$this->setKills($this->kills + $kills);
964
	}
965
966
	public function setKills(int $kills): void {
967
		if ($this->kills == $kills) {
968
			return;
969
		}
970
		$this->kills = $kills;
971
		$this->hasChanged = true;
972
	}
973
974
	public function getDeaths(): int {
975
		return $this->deaths;
976
	}
977
978
	public function increaseDeaths(int $deaths): void {
979
		if ($deaths < 0) {
980
			throw new Exception('Trying to increase negative deaths.');
981
		}
982
		$this->setDeaths($this->getDeaths() + $deaths);
983
	}
984
985
	public function setDeaths(int $deaths): void {
986
		if ($this->deaths == $deaths) {
987
			return;
988
		}
989
		$this->deaths = $deaths;
990
		$this->hasChanged = true;
991
	}
992
993
	public function getAssists(): int {
994
		return $this->assists;
995
	}
996
997
	public function increaseAssists(int $assists): void {
998
		if ($assists < 1) {
999
			throw new Exception('Must increase by a positive number.');
1000
		}
1001
		$this->assists += $assists;
1002
		$this->hasChanged = true;
1003
	}
1004
1005
	public function hasGoodAlignment(): bool {
1006
		return $this->alignment >= ALIGNMENT_GOOD;
1007
	}
1008
1009
	public function hasEvilAlignment(): bool {
1010
		return $this->alignment <= ALIGNMENT_EVIL;
1011
	}
1012
1013
	public function hasNeutralAlignment(): bool {
1014
		return !$this->hasGoodAlignment() && !$this->hasEvilAlignment();
1015
	}
1016
1017
	public function getAlignment(): int {
1018
		return $this->alignment;
1019
	}
1020
1021
	public function increaseAlignment(int $align): void {
1022
		if ($align < 0) {
1023
			throw new Exception('Trying to increase negative align.');
1024
		}
1025
		if ($align == 0) {
1026
			return;
1027
		}
1028
		$align += $this->alignment;
1029
		$this->setAlignment($align);
1030
	}
1031
1032
	public function decreaseAlignment(int $align): void {
1033
		if ($align < 0) {
1034
			throw new Exception('Trying to decrease negative align.');
1035
		}
1036
		if ($align == 0) {
1037
			return;
1038
		}
1039
		$align = $this->alignment - $align;
1040
		$this->setAlignment($align);
1041
	}
1042
1043
	public function setAlignment(int $align): void {
1044
		if ($this->alignment == $align) {
1045
			return;
1046
		}
1047
		$this->alignment = $align;
1048
		$this->hasChanged = true;
1049
	}
1050
1051
	public function getCredits(): int {
1052
		return $this->credits;
1053
	}
1054
1055
	public function getBank(): int {
1056
		return $this->bank;
1057
	}
1058
1059
	/**
1060
	 * Increases personal bank account up to the maximum allowed credits.
1061
	 * Returns the amount that was actually added to handle overflow.
1062
	 */
1063
	public function increaseBank(int $credits): int {
1064
		if ($credits == 0) {
1065
			return 0;
1066
		}
1067
		if ($credits < 0) {
1068
			throw new Exception('Trying to increase negative credits.');
1069
		}
1070
		$newTotal = min($this->bank + $credits, MAX_MONEY);
1071
		$actualAdded = $newTotal - $this->bank;
1072
		$this->setBank($newTotal);
1073
		return $actualAdded;
1074
	}
1075
1076
	public function decreaseBank(int $credits): void {
1077
		if ($credits == 0) {
1078
			return;
1079
		}
1080
		if ($credits < 0) {
1081
			throw new Exception('Trying to decrease negative credits.');
1082
		}
1083
		$newTotal = $this->bank - $credits;
1084
		$this->setBank($newTotal);
1085
	}
1086
1087
	public function setBank(int $credits): void {
1088
		if ($this->bank == $credits) {
1089
			return;
1090
		}
1091
		if ($credits < 0) {
1092
			throw new Exception('Trying to set negative credits.');
1093
		}
1094
		if ($credits > MAX_MONEY) {
1095
			throw new Exception('Trying to set more than max credits.');
1096
		}
1097
		$this->bank = $credits;
1098
		$this->hasChanged = true;
1099
	}
1100
1101
	public function getExperience(): int {
1102
		return $this->experience;
1103
	}
1104
1105
	/**
1106
	 * Returns the percent progress towards the next level.
1107
	 * This value is rounded because it is used primarily in HTML img widths.
1108
	 */
1109
	public function getNextLevelPercentAcquired(): int {
1110
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1111
			return 100;
1112
		}
1113
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1113
		return max(0, min(100, /** @scrutinizer ignore-call */ IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
Loading history...
1114
	}
1115
1116
	public function getNextLevelPercentRemaining(): int {
1117
		return 100 - $this->getNextLevelPercentAcquired();
1118
	}
1119
1120
	/**
1121
	 * @return array<string, string|int>
1122
	 */
1123
	public function getNextLevel(): array {
1124
		$LEVELS = Globals::getLevelRequirements();
1125
		if (!isset($LEVELS[$this->getLevelID() + 1])) {
1126
			return $LEVELS[$this->getLevelID()]; //Return current level experience if on last level.
1127
		}
1128
		return $LEVELS[$this->getLevelID() + 1];
1129
	}
1130
1131
	public function getNextLevelExperience(): int {
1132
		return $this->getNextLevel()['Requirement'];
1133
	}
1134
1135
	public function getThisLevelExperience(): int {
1136
		$LEVELS = Globals::getLevelRequirements();
1137
		return $LEVELS[$this->getLevelID()]['Requirement'];
1138
	}
1139
1140
	public function setExperience(int $experience): void {
1141
		if ($this->experience == $experience) {
1142
			return;
1143
		}
1144
		if ($experience < MIN_EXPERIENCE) {
1145
			$experience = MIN_EXPERIENCE;
1146
		}
1147
		if ($experience > MAX_EXPERIENCE) {
1148
			$experience = MAX_EXPERIENCE;
1149
		}
1150
		$this->experience = $experience;
1151
		$this->hasChanged = true;
1152
1153
		// Since exp has changed, invalidate the player level so that it can
1154
		// be recomputed next time it is queried (in case it has changed).
1155
		$this->level = null;
1156
	}
1157
1158
	/**
1159
	 * Increases onboard credits up to the maximum allowed credits.
1160
	 * Returns the amount that was actually added to handle overflow.
1161
	 */
1162
	public function increaseCredits(int $credits): int {
1163
		if ($credits == 0) {
1164
			return 0;
1165
		}
1166
		if ($credits < 0) {
1167
			throw new Exception('Trying to increase negative credits.');
1168
		}
1169
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1170
		$actualAdded = $newTotal - $this->credits;
1171
		$this->setCredits($newTotal);
1172
		return $actualAdded;
1173
	}
1174
1175
	public function decreaseCredits(int $credits): void {
1176
		if ($credits == 0) {
1177
			return;
1178
		}
1179
		if ($credits < 0) {
1180
			throw new Exception('Trying to decrease negative credits.');
1181
		}
1182
		$newTotal = $this->credits - $credits;
1183
		$this->setCredits($newTotal);
1184
	}
1185
1186
	public function setCredits(int $credits): void {
1187
		if ($this->credits == $credits) {
1188
			return;
1189
		}
1190
		if ($credits < 0) {
1191
			throw new Exception('Trying to set negative credits.');
1192
		}
1193
		if ($credits > MAX_MONEY) {
1194
			throw new Exception('Trying to set more than max credits.');
1195
		}
1196
		$this->credits = $credits;
1197
		$this->hasChanged = true;
1198
	}
1199
1200
	public function increaseExperience(int $experience): void {
1201
		if ($experience < 0) {
1202
			throw new Exception('Trying to increase negative experience.');
1203
		}
1204
		if ($experience == 0) {
1205
			return;
1206
		}
1207
		$newExperience = $this->experience + $experience;
1208
		$this->setExperience($newExperience);
1209
		$this->increaseHOF($experience, ['Experience', 'Total', 'Gain'], HOF_PUBLIC);
1210
	}
1211
	public function decreaseExperience(int $experience): void {
1212
		if ($experience < 0) {
1213
			throw new Exception('Trying to decrease negative experience.');
1214
		}
1215
		if ($experience == 0) {
1216
			return;
1217
		}
1218
		$newExperience = $this->experience - $experience;
1219
		$this->setExperience($newExperience);
1220
		$this->increaseHOF($experience, ['Experience', 'Total', 'Loss'], HOF_PUBLIC);
1221
	}
1222
1223
	public function isLandedOnPlanet(): bool {
1224
		return $this->landedOnPlanet;
1225
	}
1226
1227
	public function setLandedOnPlanet(bool $bool): void {
1228
		if ($this->landedOnPlanet == $bool) {
1229
			return;
1230
		}
1231
		$this->landedOnPlanet = $bool;
1232
		$this->hasChanged = true;
1233
	}
1234
1235
	/**
1236
	 * Returns the numerical level of the player (e.g. 1-50).
1237
	 */
1238
	public function getLevelID(): int {
1239
		// The level is cached for performance reasons unless `setExperience`
1240
		// is called and the player's experience changes.
1241
		if (!isset($this->level)) {
1242
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1243
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1244
				if ($this->getExperience() >= $require['Requirement']) {
1245
					continue;
1246
				}
1247
				$this->level = $level_id - 1;
1248
				return $this->level;
1249
			}
1250
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1251
		}
1252
		return $this->level;
1253
	}
1254
1255
	public function getLevelName(): string {
1256
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1257
		if ($this->isPresident()) {
1258
			$level_name = '<img src="images/council_president.png" title="' . Race::getName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1259
		}
1260
		return $level_name;
1261
	}
1262
1263
	public function getMaxLevel(): int {
1264
		return max(array_keys(Globals::getLevelRequirements()));
1265
	}
1266
1267
	public function getPlayerID(): int {
1268
		return $this->playerID;
1269
	}
1270
1271
	/**
1272
	 * Returns the player name.
1273
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1274
	 */
1275
	public function getPlayerName(): string {
1276
		return $this->playerName;
1277
	}
1278
1279
	public function setPlayerName(string $name): void {
1280
		$this->playerName = $name;
1281
		$this->hasChanged = true;
1282
	}
1283
1284
	/**
1285
	 * Returns the decorated player name, suitable for HTML display.
1286
	 */
1287
	public function getDisplayName(bool $includeAlliance = false): string {
1288
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1289
		$return = get_colored_text($this->getAlignment(), $name);
1290
		if ($this->isNPC()) {
1291
			$return .= ' <span class="npcColour">[NPC]</span>';
1292
		}
1293
		if ($includeAlliance) {
1294
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1295
		}
1296
		return $return;
1297
	}
1298
1299
	public function getBBLink(): string {
1300
			return '[player=' . $this->getPlayerID() . ']';
1301
	}
1302
1303
	public function getLinkedDisplayName(bool $includeAlliance = true): string {
1304
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1305
		if ($includeAlliance) {
1306
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1307
		}
1308
		return $return;
1309
	}
1310
1311
	/**
1312
	 * Change a player's name, with name validation.
1313
	 *
1314
	 * @throws Smr\Exceptions\UserError When the new name is not permitted.
1315
	 */
1316
	public function changePlayerName(string $name): void {
1317
		// Check if the player already has this name (case-sensitive)
1318
		if ($this->getPlayerName() == $name) {
1319
			throw new UserError('Your player already has that name!');
1320
		}
1321
1322
		// Make sure the name passes some basic character requirements
1323
		DisplayNameValidator::validate($name);
1324
1325
		// Check if name is in use by any other player.
1326
		try {
1327
			$other = self::getPlayerByPlayerName($name, $this->getGameID());
1328
			// The player_name field has case-insensitive collation, so if we
1329
			// find our own player, then it is because the new name has a
1330
			// different case (since we did a case-sensitive identity check
1331
			// above), and we allow it to be changed.
1332
			if ($this->equals($other)) {
1333
				throw new PlayerNotFound();
1334
			}
1335
			throw new UserError('That name is already being used in this game!');
1336
		} catch (PlayerNotFound) {
1337
			// Name is not in use, continue.
1338
		}
1339
1340
		$this->setPlayerName($name);
1341
	}
1342
1343
	/**
1344
	 * Use this method when the player is changing their own name.
1345
	 * This will flag the player as having used their free name change.
1346
	 */
1347
	public function changePlayerNameByPlayer(string $playerName): void {
1348
		$this->changePlayerName($playerName);
1349
		$this->setNameChanged(true);
1350
	}
1351
1352
	public function isNameChanged(): bool {
1353
		return $this->nameChanged;
1354
	}
1355
1356
	public function setNameChanged(bool $bool): void {
1357
		$this->nameChanged = $bool;
1358
		$this->hasChanged = true;
1359
	}
1360
1361
	public function isRaceChanged(): bool {
1362
		return $this->raceChanged;
1363
	}
1364
1365
	public function setRaceChanged(bool $raceChanged): void {
1366
		$this->raceChanged = $raceChanged;
1367
		$this->hasChanged = true;
1368
	}
1369
1370
	public function canChangeRace(): bool {
1371
		return !$this->isRaceChanged() && (Epoch::time() - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1372
	}
1373
1374
	public static function getColouredRaceNameOrDefault(int $otherRaceID, self $player = null, bool $linked = false): string {
1375
		$relations = 0;
1376
		if ($player !== null) {
1377
			$relations = $player->getRelation($otherRaceID);
1378
		}
1379
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1380
	}
1381
1382
	public function getColouredRaceName(int $otherRaceID, bool $linked = false): string {
1383
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1384
	}
1385
1386
	public function setRaceID(int $raceID): void {
1387
		if ($this->raceID == $raceID) {
1388
			return;
1389
		}
1390
		$this->raceID = $raceID;
1391
		$this->hasChanged = true;
1392
	}
1393
1394
	public function isAllianceLeader(bool $forceUpdate = false): bool {
1395
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1396
	}
1397
1398
	public function getAlliance(bool $forceUpdate = false): SmrAlliance {
1399
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1400
	}
1401
1402
	public function getAllianceID(): int {
1403
		return $this->allianceID;
1404
	}
1405
1406
	public function hasAlliance(): bool {
1407
		return $this->getAllianceID() != 0;
1408
	}
1409
1410
	protected function setAllianceID(int $ID): void {
1411
		if ($this->allianceID == $ID) {
1412
			return;
1413
		}
1414
		$this->allianceID = $ID;
1415
		if ($this->allianceID != 0) {
1416
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1417
			$this->db->write('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1418
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1419
		}
1420
		$this->hasChanged = true;
1421
	}
1422
1423
	public function getAllianceBBLink(): string {
1424
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1425
	}
1426
1427
	public function getAllianceDisplayName(bool $linked = false, bool $includeAllianceID = false): string {
1428
		if (!$this->hasAlliance()) {
1429
			return 'No Alliance';
1430
		}
1431
		return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1432
	}
1433
1434
	public function getAllianceRole(int $allianceID = null): int {
1435
		if ($allianceID === null) {
1436
			$allianceID = $this->getAllianceID();
1437
		}
1438
		if (!isset($this->allianceRoles[$allianceID])) {
1439
			$this->allianceRoles[$allianceID] = 0;
1440
			$dbResult = $this->db->read('SELECT role_id
1441
						FROM player_has_alliance_role
1442
						WHERE ' . $this->SQL . '
1443
						AND alliance_id=' . $this->db->escapeNumber($allianceID));
1444
			if ($dbResult->hasRecord()) {
1445
				$this->allianceRoles[$allianceID] = $dbResult->record()->getInt('role_id');
1446
			}
1447
		}
1448
		return $this->allianceRoles[$allianceID];
1449
	}
1450
1451
	public function leaveAlliance(self $kickedBy = null): void {
1452
		$alliance = $this->getAlliance();
1453
		if ($kickedBy !== null) {
1454
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1455
			$this->actionTaken('PlayerKicked', ['Alliance' => $alliance, 'Player' => $kickedBy]);
1456
			$kickedBy->actionTaken('KickPlayer', ['Alliance' => $alliance, 'Player' => $this]);
1457
		} elseif ($this->isAllianceLeader()) {
1458
			$this->actionTaken('DisbandAlliance', ['Alliance' => $alliance]);
1459
		} else {
1460
			$this->actionTaken('LeaveAlliance', ['Alliance' => $alliance]);
1461
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1462
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1463
			}
1464
		}
1465
1466
		// Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1467
		if (!$this->isAllianceLeader() && !$alliance->isNHA()) {
1468
			$this->setAllianceJoinable(Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH);
1469
			$alliance->getLeader()->setAllianceJoinable(Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH); //We set the joinable time for leader here, that way a single player alliance won't cause a player to wait before switching.
1470
		}
1471
1472
		$this->setAllianceID(0);
1473
		$this->db->write('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1474
1475
		// Update the alliance cache
1476
		unset(self::$CACHE_ALLIANCE_PLAYERS[$this->gameID][$alliance->getAllianceID()][$this->accountID]);
1477
	}
1478
1479
	/**
1480
	 * Join an alliance (used for both Leader and New Member roles)
1481
	 */
1482
	public function joinAlliance(int $allianceID): void {
1483
		$this->setAllianceID($allianceID);
1484
		$alliance = $this->getAlliance();
1485
1486
		if (!$this->isAllianceLeader()) {
1487
			// Do not throw an exception if the NHL account doesn't exist.
1488
			try {
1489
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1490
			} catch (AccountNotFound $e) {
1491
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1492
					throw $e;
1493
				}
1494
			}
1495
1496
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1497
		} else {
1498
			$roleID = ALLIANCE_ROLE_LEADER;
1499
		}
1500
		$this->db->insert('player_has_alliance_role', [
1501
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1502
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1503
			'role_id' => $this->db->escapeNumber($roleID),
1504
			'alliance_id' => $this->db->escapeNumber($this->getAllianceID()),
1505
		]);
1506
1507
		$this->actionTaken('JoinAlliance', ['Alliance' => $alliance]);
1508
	}
1509
1510
	public function getAllianceJoinable(): int {
1511
		return $this->allianceJoinable;
1512
	}
1513
1514
	private function setAllianceJoinable(int $time): void {
1515
		if ($this->allianceJoinable == $time) {
1516
			return;
1517
		}
1518
		$this->allianceJoinable = $time;
1519
		$this->hasChanged = true;
1520
	}
1521
1522
	/**
1523
	 * Invites player with $accountID to this player's alliance.
1524
	 */
1525
	public function sendAllianceInvitation(int $accountID, string $message, int $expires): void {
1526
		if (!$this->hasAlliance()) {
1527
			throw new Exception('Must be in an alliance to send alliance invitations');
1528
		}
1529
		// Send message to invited player
1530
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1531
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1532
	}
1533
1534
	public function isCombatDronesKamikazeOnMines(): bool {
1535
		return $this->combatDronesKamikazeOnMines;
1536
	}
1537
1538
	public function setCombatDronesKamikazeOnMines(bool $bool): void {
1539
		if ($this->combatDronesKamikazeOnMines == $bool) {
1540
			return;
1541
		}
1542
		$this->combatDronesKamikazeOnMines = $bool;
1543
		$this->hasChanged = true;
1544
	}
1545
1546
	protected function getPersonalRelationsData(): void {
1547
		if (!isset($this->personalRelations)) {
1548
			//get relations
1549
			$this->personalRelations = [];
1550
			foreach (Race::getAllIDs() as $raceID) {
1551
				$this->personalRelations[$raceID] = 0;
1552
			}
1553
			$dbResult = $this->db->read('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL);
1554
			foreach ($dbResult->records() as $dbRecord) {
1555
				$this->personalRelations[$dbRecord->getInt('race_id')] = $dbRecord->getInt('relation');
1556
			}
1557
		}
1558
	}
1559
1560
	/**
1561
	 * @return array<int, int>
1562
	 */
1563
	public function getPersonalRelations(): array {
1564
		$this->getPersonalRelationsData();
1565
		return $this->personalRelations;
1566
	}
1567
1568
	/**
1569
	 * Get personal relations with a race
1570
	 */
1571
	public function getPersonalRelation(int $raceID): int {
1572
		$rels = $this->getPersonalRelations();
1573
		return $rels[$raceID];
1574
	}
1575
1576
	/**
1577
	 * Get total relations with all races (personal + political)
1578
	 *
1579
	 * @return array<int, int>
1580
	 */
1581
	public function getRelations(): array {
1582
		if (!isset($this->relations)) {
1583
			//get relations
1584
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1585
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1586
			$this->relations = [];
1587
			foreach (Race::getAllIDs() as $raceID) {
1588
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1589
			}
1590
		}
1591
		return $this->relations;
1592
	}
1593
1594
	/**
1595
	 * Get total relations with a race (personal + political)
1596
	 */
1597
	public function getRelation(int $raceID): int {
1598
		$rels = $this->getRelations();
1599
		return $rels[$raceID];
1600
	}
1601
1602
	/**
1603
	 * Increases personal relations from trading $numGoods units with the race
1604
	 * of the port given by $raceID.
1605
	 */
1606
	public function increaseRelationsByTrade(int $numGoods, int $raceID): void {
1607
		$relations = ICeil(min($numGoods, 300) / 30);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1607
		$relations = /** @scrutinizer ignore-call */ ICeil(min($numGoods, 300) / 30);
Loading history...
1608
		//Cap relations to a max of 1 after 500 have been reached
1609
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1610
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1611
		}
1612
		$this->increaseRelations($relations, $raceID);
1613
	}
1614
1615
	/**
1616
	 * Decreases personal relations from trading failures, e.g. rejected
1617
	 * bargaining and getting caught stealing.
1618
	 */
1619
	public function decreaseRelationsByTrade(int $numGoods, int $raceID): void {
1620
		$relations = ICeil(min($numGoods, 300) / 30);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1620
		$relations = /** @scrutinizer ignore-call */ ICeil(min($numGoods, 300) / 30);
Loading history...
1621
		$this->decreaseRelations($relations, $raceID);
1622
	}
1623
1624
	/**
1625
	 * Increase personal relations.
1626
	 */
1627
	public function increaseRelations(int $relations, int $raceID): void {
1628
		if ($relations < 0) {
1629
			throw new Exception('Trying to increase negative relations.');
1630
		}
1631
		if ($relations == 0) {
1632
			return;
1633
		}
1634
		$relations += $this->getPersonalRelation($raceID);
1635
		$this->setRelations($relations, $raceID);
1636
	}
1637
1638
	/**
1639
	 * Decrease personal relations.
1640
	 */
1641
	public function decreaseRelations(int $relations, int $raceID): void {
1642
		if ($relations < 0) {
1643
			throw new Exception('Trying to decrease negative relations.');
1644
		}
1645
		if ($relations == 0) {
1646
			return;
1647
		}
1648
		$relations = $this->getPersonalRelation($raceID) - $relations;
1649
		$this->setRelations($relations, $raceID);
1650
	}
1651
1652
	/**
1653
	 * Set personal relations.
1654
	 */
1655
	public function setRelations(int $relations, int $raceID): void {
1656
		$this->getRelations();
1657
		if ($this->personalRelations[$raceID] == $relations) {
1658
			return;
1659
		}
1660
		if ($relations < MIN_RELATIONS) {
1661
			$relations = MIN_RELATIONS;
1662
		}
1663
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1663
		$relationsDiff = /** @scrutinizer ignore-call */ IRound($relations - $this->personalRelations[$raceID]);
Loading history...
1664
		$this->personalRelations[$raceID] = $relations;
1665
		$this->relations[$raceID] += $relationsDiff;
1666
		$this->db->replace('player_has_relation', [
1667
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1668
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1669
			'race_id' => $this->db->escapeNumber($raceID),
1670
			'relation' => $this->db->escapeNumber($this->personalRelations[$raceID]),
1671
		]);
1672
	}
1673
1674
	/**
1675
	 * Set any starting personal relations bonuses or penalties.
1676
	 */
1677
	public function giveStartingRelations(): void {
1678
		if ($this->getRaceID() === RACE_ALSKANT) {
1679
			// Give Alskants bonus personal relations to start.
1680
			foreach (Race::getAllIDs() as $raceID) {
1681
				$this->setRelations(ALSKANT_BONUS_RELATIONS, $raceID);
1682
			}
1683
		}
1684
	}
1685
1686
	public function getLastNewsUpdate(): int {
1687
		return $this->lastNewsUpdate;
1688
	}
1689
1690
	private function setLastNewsUpdate(int $time): void {
1691
		if ($this->lastNewsUpdate == $time) {
1692
			return;
1693
		}
1694
		$this->lastNewsUpdate = $time;
1695
		$this->hasChanged = true;
1696
	}
1697
1698
	public function updateLastNewsUpdate(): void {
1699
		$this->setLastNewsUpdate(Epoch::time());
1700
	}
1701
1702
	public function getLastPort(): int {
1703
		return $this->lastPort;
1704
	}
1705
1706
	public function setLastPort(int $lastPort): void {
1707
		if ($this->lastPort == $lastPort) {
1708
			return;
1709
		}
1710
		$this->lastPort = $lastPort;
1711
		$this->hasChanged = true;
1712
	}
1713
1714
	public function getPlottedCourse(): ?Path {
1715
		if (!isset($this->plottedCourse)) {
1716
			// check if we have a course plotted
1717
			$dbResult = $this->db->read('SELECT course FROM player_plotted_course WHERE ' . $this->SQL);
1718
1719
			if ($dbResult->hasRecord()) {
1720
				// get the course back
1721
				$this->plottedCourse = $dbResult->record()->getObject('course');
1722
			} else {
1723
				$this->plottedCourse = null;
1724
			}
1725
		}
1726
1727
		// Update the plotted course if we have moved since the last query
1728
		if ($this->plottedCourse !== null && $this->plottedCourse->getStartSectorID() != $this->getSectorID()) {
1729
			if ($this->plottedCourse->getEndSectorID() == $this->getSectorID()) {
1730
				// We have reached our destination
1731
				$this->deletePlottedCourse();
1732
			} elseif ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1733
				// We have walked into the next sector of the course
1734
				$this->plottedCourse->followPath();
1735
				$this->setPlottedCourse($this->plottedCourse);
1736
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1737
				// We have skipped to some later sector in the course
1738
				$this->plottedCourse->skipToSector($this->getSectorID());
1739
				$this->setPlottedCourse($this->plottedCourse);
1740
			}
1741
		}
1742
		return $this->plottedCourse;
1743
	}
1744
1745
	public function setPlottedCourse(Path $plottedCourse): void {
1746
		$this->plottedCourse = $plottedCourse;
1747
		$this->db->replace('player_plotted_course', [
1748
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1749
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1750
			'course' => $this->db->escapeObject($this->plottedCourse),
1751
		]);
1752
	}
1753
1754
	public function hasPlottedCourse(): bool {
1755
		return $this->getPlottedCourse() !== null;
1756
	}
1757
1758
	public function isPartOfCourse(SmrSector $sector): bool {
1759
		return $this->getPlottedCourse()?->isInPath($sector->getSectorID()) === true;
1760
	}
1761
1762
	public function deletePlottedCourse(): void {
1763
		$this->plottedCourse = null;
1764
		$this->db->write('DELETE FROM player_plotted_course WHERE ' . $this->SQL);
1765
	}
1766
1767
	/**
1768
	 * Computes the turn cost and max misjump between current and target sector
1769
	 *
1770
	 * @return array<string, int>
1771
	 */
1772
	public function getJumpInfo(SmrSector $targetSector): array {
1773
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1774
		if ($path === false) {
1775
			throw new UserError('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1776
		}
1777
		$distance = $path->getDistance();
1778
1779
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1779
		$turnCost = max(TURNS_JUMP_MINIMUM, /** @scrutinizer ignore-call */ IRound($distance * TURNS_PER_JUMP_DISTANCE));
Loading history...
1780
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1781
		return ['turn_cost' => $turnCost, 'max_misjump' => $maxMisjump];
1782
	}
1783
1784
	public function __sleep() {
1785
		return ['accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName', 'npc'];
1786
	}
1787
1788
	/**
1789
	 * @return array<int, StoredDestination>
1790
	 */
1791
	public function getStoredDestinations(): array {
1792
		if (!isset($this->storedDestinations)) {
1793
			$this->storedDestinations = [];
1794
			$dbResult = $this->db->read('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1795
			foreach ($dbResult->records() as $dbRecord) {
1796
				$sectorID = $dbRecord->getInt('sector_id');
1797
				$this->storedDestinations[$sectorID] = new StoredDestination(
1798
					sectorID: $sectorID,
1799
					label: $dbRecord->getString('label'),
1800
					offsetTop: $dbRecord->getInt('offset_top'),
1801
					offsetLeft: $dbRecord->getInt('offset_left'),
1802
				);
1803
			}
1804
		}
1805
		return $this->storedDestinations;
1806
	}
1807
1808
	public function moveDestinationButton(int $sectorID, int $offsetTop, int $offsetLeft): void {
1809
		$this->getStoredDestinations(); // make sure property is initialized
1810
1811
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1812
			throw new UserError('The saved sector must be in the box!');
1813
		}
1814
1815
		if (!isset($this->storedDestinations[$sectorID])) {
1816
			throw new UserError('You do not have a saved sector for #' . $sectorID);
1817
		}
1818
1819
		// Replace destination with updated offsets
1820
		$this->storedDestinations[$sectorID] = new StoredDestination(
1821
			sectorID: $sectorID,
1822
			label: $this->storedDestinations[$sectorID]->label,
1823
			offsetTop: $offsetTop,
1824
			offsetLeft: $offsetLeft,
1825
		);
1826
		$this->db->write('
1827
			UPDATE player_stored_sector
1828
				SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1829
			WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID));
1830
	}
1831
1832
	public function addDestinationButton(int $sectorID, string $label): void {
1833
		$this->getStoredDestinations(); // make sure property is initialized
1834
1835
		if (!SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1836
			throw new UserError('You want to add a non-existent sector?');
1837
		}
1838
1839
		// sector already stored ?
1840
		if (isset($this->storedDestinations[$sectorID])) {
1841
			throw new UserError('Sector already stored!');
1842
		}
1843
1844
		$this->storedDestinations[$sectorID] = new StoredDestination(
1845
			label: $label,
1846
			sectorID: $sectorID,
1847
			offsetTop: 1,
1848
			offsetLeft: 1,
1849
		);
1850
1851
		$this->db->insert('player_stored_sector', [
1852
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1853
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1854
			'sector_id' => $this->db->escapeNumber($sectorID),
1855
			'label' => $this->db->escapeString($label),
1856
			'offset_top' => 1,
1857
			'offset_left' => 1,
1858
		]);
1859
	}
1860
1861
	public function deleteDestinationButton(int $sectorID): void {
1862
		$this->getStoredDestinations(); // make sure property is initialized
1863
1864
		if (!isset($this->storedDestinations[$sectorID])) {
1865
			throw new Exception('Could not find stored destination');
1866
		}
1867
1868
		$this->db->write('
1869
			DELETE FROM player_stored_sector
1870
			WHERE ' . $this->SQL . '
1871
			AND sector_id = ' . $this->db->escapeNumber($sectorID));
1872
		unset($this->storedDestinations[$sectorID]);
1873
	}
1874
1875
	/**
1876
	 * @return array<string, array<string, mixed>>
1877
	 */
1878
	public function getTickers(): array {
1879
		if (!isset($this->tickers)) {
1880
			$this->tickers = [];
1881
			//get ticker info
1882
			$dbResult = $this->db->read('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(Epoch::time()));
1883
			foreach ($dbResult->records() as $dbRecord) {
1884
				$this->tickers[$dbRecord->getString('type')] = [
1885
					'Type' => $dbRecord->getString('type'),
1886
					'Time' => $dbRecord->getInt('time'),
1887
					'Expires' => $dbRecord->getInt('expires'),
1888
					'Recent' => $dbRecord->getString('recent'),
1889
				];
1890
			}
1891
		}
1892
		return $this->tickers;
1893
	}
1894
1895
	public function hasTickers(): bool {
1896
		return count($this->getTickers()) > 0;
1897
	}
1898
1899
	/**
1900
	 * @return array<string, mixed>|false
1901
	 */
1902
	public function getTicker(string $tickerType): array|false {
1903
		$tickers = $this->getTickers();
1904
		if (isset($tickers[$tickerType])) {
1905
			return $tickers[$tickerType];
1906
		}
1907
		return false;
1908
	}
1909
1910
	public function hasTicker(string $tickerType): bool {
1911
		return $this->getTicker($tickerType) !== false;
1912
	}
1913
1914
	/**
1915
	 * @return array<string, mixed>
1916
	 */
1917
	public function shootForces(SmrForce $forces): array {
1918
		return $this->getShip()->shootForces($forces);
1919
	}
1920
1921
	/**
1922
	 * @return array<string, mixed>
1923
	 */
1924
	public function shootPort(SmrPort $port): array {
1925
		return $this->getShip()->shootPort($port);
1926
	}
1927
1928
	/**
1929
	 * @return array<string, mixed>
1930
	 */
1931
	public function shootPlanet(SmrPlanet $planet): array {
1932
		return $this->getShip()->shootPlanet($planet);
1933
	}
1934
1935
	/**
1936
	 * @param array<AbstractSmrPlayer> $targetPlayers
1937
	 * @return array<string, mixed>
1938
	 */
1939
	public function shootPlayers(array $targetPlayers): array {
1940
		return $this->getShip()->shootPlayers($targetPlayers);
1941
	}
1942
1943
	public function getMilitaryPayment(): int {
1944
		return $this->militaryPayment;
1945
	}
1946
1947
	public function hasMilitaryPayment(): bool {
1948
		return $this->getMilitaryPayment() > 0;
1949
	}
1950
1951
	public function setMilitaryPayment(int $amount): void {
1952
		if ($this->militaryPayment == $amount) {
1953
			return;
1954
		}
1955
		$this->militaryPayment = $amount;
1956
		$this->hasChanged = true;
1957
	}
1958
1959
	public function increaseMilitaryPayment(int $amount): void {
1960
		if ($amount < 0) {
1961
			throw new Exception('Trying to increase negative military payment.');
1962
		}
1963
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1964
	}
1965
1966
	public function decreaseMilitaryPayment(int $amount): void {
1967
		if ($amount < 0) {
1968
			throw new Exception('Trying to decrease negative military payment.');
1969
		}
1970
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1971
	}
1972
1973
	protected function getBountiesData(): void {
1974
		if (!isset($this->bounties)) {
1975
			$this->bounties = [];
1976
			$dbResult = $this->db->read('SELECT * FROM bounty WHERE ' . $this->SQL);
1977
			foreach ($dbResult->records() as $dbRecord) {
1978
				$this->bounties[$dbRecord->getInt('bounty_id')] = [
1979
							'Amount' => $dbRecord->getInt('amount'),
1980
							'SmrCredits' => $dbRecord->getInt('smr_credits'),
1981
							'Type' => BountyType::from($dbRecord->getString('type')),
1982
							'Claimer' => $dbRecord->getInt('claimer_id'),
1983
							'Time' => $dbRecord->getInt('time'),
1984
							'ID' => $dbRecord->getInt('bounty_id'),
1985
							'New' => false];
1986
			}
1987
		}
1988
	}
1989
1990
	/**
1991
	 * Get bounties that can be claimed by this player.
1992
	 *
1993
	 * @return array<array<string, mixed>>
1994
	 */
1995
	public function getClaimableBounties(?BountyType $type = null): array {
1996
		$bounties = [];
1997
		$query = 'SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID());
1998
		$query .= match ($type) {
1999
			null => '',
2000
			default => ' AND type=' . $this->db->escapeString($type->value),
2001
		};
2002
		$dbResult = $this->db->read($query);
2003
		foreach ($dbResult->records() as $dbRecord) {
2004
			$bounties[] = [
2005
				'player' => self::getPlayer($dbRecord->getInt('account_id'), $this->getGameID()),
2006
				'bounty_id' => $dbRecord->getInt('bounty_id'),
2007
				'credits' => $dbRecord->getInt('amount'),
2008
				'smr_credits' => $dbRecord->getInt('smr_credits'),
2009
			];
2010
		}
2011
		return $bounties;
2012
	}
2013
2014
	/**
2015
	 * @return array<int, array<string, mixed>>
2016
	 */
2017
	public function getBounties(): array {
2018
		$this->getBountiesData();
2019
		return $this->bounties;
2020
	}
2021
2022
	public function hasBounties(): bool {
2023
		return count($this->getBounties()) > 0;
2024
	}
2025
2026
	/**
2027
	 * @return array<string, mixed>
2028
	 */
2029
	protected function getBounty(int $bountyID): array {
2030
		if (!$this->hasBounty($bountyID)) {
2031
			throw new Exception('BountyID does not exist: ' . $bountyID);
2032
		}
2033
		return $this->bounties[$bountyID];
2034
	}
2035
2036
	public function hasBounty(int $bountyID): bool {
2037
		$bounties = $this->getBounties();
2038
		return isset($bounties[$bountyID]);
2039
	}
2040
2041
	/**
2042
	 * @return array<string, mixed>
2043
	 */
2044
	protected function createBounty(BountyType $type): array {
2045
		$bounty = [
2046
			'Amount' => 0,
2047
			'SmrCredits' => 0,
2048
			'Type' => $type,
2049
			'Claimer' => 0,
2050
			'Time' => Epoch::time(),
2051
			'ID' => $this->getNextBountyID(),
2052
			'New' => true,
2053
		];
2054
		$this->setBounty($bounty);
2055
		return $bounty;
2056
	}
2057
2058
	protected function getNextBountyID(): int {
2059
		if (!$this->hasBounties()) {
2060
			return 0;
2061
		}
2062
		return max(array_keys($this->getBounties())) + 1;
2063
	}
2064
2065
	/**
2066
	 * @param array<string, mixed> $bounty
2067
	 */
2068
	protected function setBounty(array $bounty): void {
2069
		$this->bounties[$bounty['ID']] = $bounty;
2070
		$this->hasBountyChanged[$bounty['ID']] = true;
2071
	}
2072
2073
	/**
2074
	 * @return array<string, mixed>
2075
	 */
2076
	public function getCurrentBounty(BountyType $type): array {
2077
		$bounties = $this->getBounties();
2078
		foreach ($bounties as $bounty) {
2079
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
2080
				return $bounty;
2081
			}
2082
		}
2083
		return $this->createBounty($type);
2084
	}
2085
2086
	public function hasCurrentBounty(BountyType $type): bool {
2087
		$bounties = $this->getBounties();
2088
		foreach ($bounties as $bounty) {
2089
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
2090
				return true;
2091
			}
2092
		}
2093
		return false;
2094
	}
2095
2096
	public function getCurrentBountyAmount(BountyType $type): int {
2097
		$bounty = $this->getCurrentBounty($type);
2098
		return $bounty['Amount'];
2099
	}
2100
2101
	protected function setCurrentBountyAmount(BountyType $type, int $amount): void {
2102
		$bounty = $this->getCurrentBounty($type);
2103
		if ($bounty['Amount'] == $amount) {
2104
			return;
2105
		}
2106
		$bounty['Amount'] = $amount;
2107
		$this->setBounty($bounty);
2108
	}
2109
2110
	public function increaseCurrentBountyAmount(BountyType $type, int $amount): void {
2111
		if ($amount < 0) {
2112
			throw new Exception('Trying to increase negative current bounty.');
2113
		}
2114
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
2115
	}
2116
2117
	public function decreaseCurrentBountyAmount(BountyType $type, int $amount): void {
2118
		if ($amount < 0) {
2119
			throw new Exception('Trying to decrease negative current bounty.');
2120
		}
2121
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
2122
	}
2123
2124
	protected function getCurrentBountySmrCredits(BountyType $type): int {
2125
		$bounty = $this->getCurrentBounty($type);
2126
		return $bounty['SmrCredits'];
2127
	}
2128
2129
	protected function setCurrentBountySmrCredits(BountyType $type, int $credits): void {
2130
		$bounty = $this->getCurrentBounty($type);
2131
		if ($bounty['SmrCredits'] == $credits) {
2132
			return;
2133
		}
2134
		$bounty['SmrCredits'] = $credits;
2135
		$this->setBounty($bounty);
2136
	}
2137
2138
	public function increaseCurrentBountySmrCredits(BountyType $type, int $credits): void {
2139
		if ($credits < 0) {
2140
			throw new Exception('Trying to increase negative current bounty.');
2141
		}
2142
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
2143
	}
2144
2145
	public function decreaseCurrentBountySmrCredits(BountyType $type, int $credits): void {
2146
		if ($credits < 0) {
2147
			throw new Exception('Trying to decrease negative current bounty.');
2148
		}
2149
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2150
	}
2151
2152
	public function setBountiesClaimable(self $claimer): void {
2153
		foreach ($this->getBounties() as $bounty) {
2154
			if ($bounty['Claimer'] == 0) {
2155
				$bounty['Claimer'] = $claimer->getAccountID();
2156
				$this->setBounty($bounty);
2157
			}
2158
		}
2159
	}
2160
2161
	protected function getHOFData(): void {
2162
		if (!isset($this->HOF)) {
2163
			//Get Player HOF
2164
			$dbResult = $this->db->read('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2165
			$this->HOF = [];
2166
			foreach ($dbResult->records() as $dbRecord) {
2167
				$this->HOF[$dbRecord->getString('type')] = $dbRecord->getFloat('amount');
2168
			}
2169
			self::getHOFVis();
2170
		}
2171
	}
2172
2173
	/**
2174
	 * @return array<string, string>
2175
	 */
2176
	public static function getHOFVis(): array {
2177
		if (!isset(self::$HOFVis)) {
2178
			//Get Player HOF Vis
2179
			$db = Database::getInstance();
2180
			$dbResult = $db->read('SELECT type,visibility FROM hof_visibility');
2181
			self::$HOFVis = [];
2182
			foreach ($dbResult->records() as $dbRecord) {
2183
				self::$HOFVis[$dbRecord->getString('type')] = $dbRecord->getString('visibility');
2184
			}
2185
			// Add non-database types
2186
			self::$HOFVis[HOF_TYPE_DONATION] = HOF_PUBLIC;
2187
			self::$HOFVis[HOF_TYPE_USER_SCORE] = HOF_PUBLIC;
2188
		}
2189
		return self::$HOFVis;
2190
	}
2191
2192
	/**
2193
	 * @param array<string> $typeList
2194
	 */
2195
	public function getHOF(array $typeList): float {
2196
		$this->getHOFData();
2197
		return $this->HOF[implode(':', $typeList)] ?? 0;
2198
	}
2199
2200
	/**
2201
	 * @param array<string> $typeList
2202
	 */
2203
	public function increaseHOF(float $amount, array $typeList, string $visibility): void {
2204
		if ($amount < 0) {
2205
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2206
		}
2207
		if ($amount == 0) {
2208
			return;
2209
		}
2210
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2211
	}
2212
2213
	/**
2214
	 * @param array<string> $typeList
2215
	 */
2216
	public function decreaseHOF(float $amount, array $typeList, string $visibility): void {
2217
		if ($amount < 0) {
2218
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2219
		}
2220
		if ($amount == 0) {
2221
			return;
2222
		}
2223
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2224
	}
2225
2226
	/**
2227
	 * @param array<string> $typeList
2228
	 */
2229
	public function setHOF(float $amount, array $typeList, string $visibility): void {
2230
		if ($this->isNPC()) {
2231
			// Don't store HOF for NPCs.
2232
			return;
2233
		}
2234
		if ($amount < 0) {
2235
			$amount = 0;
2236
		}
2237
		if ($this->getHOF($typeList) == $amount) {
2238
			return;
2239
		}
2240
2241
		$hofType = implode(':', $typeList);
2242
		if (!isset(self::$HOFVis[$hofType])) {
2243
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2244
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2245
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2246
		}
2247
		self::$HOFVis[$hofType] = $visibility;
2248
2249
		if (!isset($this->HOF[$hofType])) {
2250
			$this->hasHOFChanged[$hofType] = self::HOF_NEW;
2251
		} else {
2252
			$this->hasHOFChanged[$hofType] = self::HOF_CHANGED;
2253
		}
2254
		$this->HOF[$hofType] = $amount;
2255
	}
2256
2257
	public function isUnderAttack(): bool {
2258
		return $this->underAttack;
2259
	}
2260
2261
	public function setUnderAttack(bool $value): void {
2262
		if ($this->underAttack === $value) {
2263
			return;
2264
		}
2265
		$this->underAttack = $value;
2266
		$this->hasChanged = true;
2267
	}
2268
2269
	public function killPlayer(int $sectorID): void {
2270
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2271
		//msg taken care of in trader_att_proc.php
2272
		// forget plotted course
2273
		$this->deletePlottedCourse();
2274
2275
		$sector->diedHere($this);
2276
2277
		// if we are in an alliance we increase their deaths
2278
		if ($this->hasAlliance()) {
2279
			$this->db->write('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2280
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()));
2281
		}
2282
2283
		// record death stat
2284
		$this->increaseHOF(1, ['Dying', 'Deaths'], HOF_PUBLIC);
2285
		//record cost of ship lost
2286
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Money', 'Cost Of Ships Lost'], HOF_PUBLIC);
2287
		// reset turns since last death
2288
		$this->setHOF(0, ['Movement', 'Turns Used', 'Since Last Death'], HOF_ALLIANCE);
2289
2290
		// Reset credits to starting amount + ship insurance
2291
		$credits = $this->getGame()->getStartingCredits();
2292
		$credits += IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2292
		$credits += /** @scrutinizer ignore-call */ IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
Loading history...
2293
		$this->setCredits($credits);
2294
2295
		$this->setSectorID($this->getHome());
2296
		$this->increaseDeaths(1);
2297
		$this->setLandedOnPlanet(false);
2298
		$this->setDead(true);
2299
		$this->setNewbieWarning(true);
2300
		$this->getShip()->getPod($this->hasNewbieStatus());
2301
		$this->setNewbieTurns(NEWBIE_TURNS_ON_DEATH);
2302
		$this->setUnderAttack(false);
2303
	}
2304
2305
	/**
2306
	 * @return array<string, mixed>
2307
	 */
2308
	public function killPlayerByPlayer(self $killer): array {
2309
		$return = [];
2310
		$msg = $this->getBBLink();
2311
2312
		if ($this->hasCustomShipName()) {
2313
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2314
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2315
		}
2316
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2317
		if ($killer->hasCustomShipName()) {
2318
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2319
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2320
		}
2321
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2322
		$this->getSector()->increaseBattles(1);
2323
		$this->db->insert('news', [
2324
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2325
			'time' => $this->db->escapeNumber(Epoch::time()),
2326
			'news_message' => $this->db->escapeString($msg),
2327
			'killer_id' => $this->db->escapeNumber($killer->getAccountID()),
2328
			'killer_alliance' => $this->db->escapeNumber($killer->getAllianceID()),
2329
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2330
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2331
		]);
2332
2333
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2334
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2335
2336
		// Dead player loses between 5% and 25% experience
2337
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2338
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2338
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2339
		$expBeforeDeath = $this->getExperience();
2340
		$this->decreaseExperience($return['DeadExp']);
2341
2342
		// Killer gains 50% of the lost exp
2343
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2343
		$return['KillerExp'] = max(0, /** @scrutinizer ignore-call */ ICeil(0.5 * $return['DeadExp']));
Loading history...
2344
		$killer->increaseExperience($return['KillerExp']);
2345
2346
		$return['KillerCredits'] = $this->getCredits();
2347
		$killer->increaseCredits($return['KillerCredits']);
2348
2349
		// The killer may change alignment
2350
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2351
		$relation = $relations[$killer->getRaceID()];
2352
2353
		$alignChangePerRelation = 0.1;
2354
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2355
			$alignChangePerRelation = 0.04;
2356
		}
2357
2358
		$killerAlignChange = IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2358
		$killerAlignChange = /** @scrutinizer ignore-call */ IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
Loading history...
2359
		if ($killerAlignChange > 0) {
2360
			$killer->increaseAlignment($killerAlignChange);
2361
		} else {
2362
			$killer->decreaseAlignment(-$killerAlignChange);
2363
		}
2364
		// War setting gives them military pay
2365
		if ($relation <= RELATIONS_WAR) {
2366
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2367
		}
2368
2369
		//check for federal bounty being offered for current port raiders;
2370
		$this->db->write('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(Epoch::time() - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2371
		$query = 'SELECT 1
2372
					FROM player_attacks_port
2373
					JOIN port USING(game_id, sector_id)
2374
					JOIN player USING(game_id, account_id)
2375
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2376
		$dbResult = $this->db->read($query);
2377
		if ($dbResult->hasRecord()) {
2378
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2379
			$this->increaseCurrentBountyAmount(BountyType::HQ, $bounty);
2380
		}
2381
2382
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2383
		$this->setBountiesClaimable($killer);
2384
2385
		// If the alignment difference is greater than 200 then a bounty may be set
2386
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2387
		$return['BountyGained'] = [
2388
			'Type' => 'None',
2389
			'Amount' => 0,
2390
		];
2391
		if ($alignmentDiff >= 200) {
2392
			// If the podded players alignment makes them deputy or member then set bounty
2393
			if ($this->hasGoodAlignment()) {
2394
				$return['BountyGained']['Type'] = BountyType::HQ;
2395
			} elseif ($this->hasEvilAlignment()) {
2396
				$return['BountyGained']['Type'] = BountyType::UG;
2397
			}
2398
2399
			if ($return['BountyGained']['Type'] != 'None') {
2400
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2401
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2402
			}
2403
		}
2404
2405
		$killingHof = ['Killing'];
2406
		if ($this->isNPC()) {
2407
			$killingHof[] = 'NPC';
2408
		}
2409
		$killer->increaseHOF($return['KillerExp'], [...$killingHof, 'Experience', 'Gained'], HOF_PUBLIC);
2410
		$killer->increaseHOF($expBeforeDeath, [...$killingHof, 'Experience', 'Of Traders Killed'], HOF_PUBLIC);
2411
		$killer->increaseHOF($return['DeadExp'], [...$killingHof, 'Experience', 'Lost By Traders Killed'], HOF_PUBLIC);
2412
2413
		$killer->increaseHOF($return['KillerCredits'], [...$killingHof, 'Money', 'Lost By Traders Killed'], HOF_PUBLIC);
2414
		$killer->increaseHOF($return['KillerCredits'], [...$killingHof, 'Money', 'Gain'], HOF_PUBLIC);
2415
		$killer->increaseHOF($this->getShip()->getCost(), [...$killingHof, 'Money', 'Cost Of Ships Killed'], HOF_PUBLIC);
2416
		$killer->increaseHOF($return['BountyGained']['Amount'], [...$killingHof, 'Money', 'Bounty Gained'], HOF_PUBLIC);
2417
2418
		if ($killerAlignChange > 0) {
2419
			$killer->increaseHOF($killerAlignChange, [...$killingHof, 'Alignment', 'Gain'], HOF_PUBLIC);
2420
		} else {
2421
			$killer->increaseHOF(-$killerAlignChange, [...$killingHof, 'Alignment', 'Loss'], HOF_PUBLIC);
2422
		}
2423
2424
		if ($this->isNPC()) {
2425
			$killer->increaseHOF(1, ['Killing', 'NPC Kills'], HOF_PUBLIC);
2426
		} elseif ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2427
			$killer->increaseHOF(1, ['Killing', 'Newbie Kills'], HOF_PUBLIC);
2428
		} else {
2429
			$killer->increaseKills(1);
2430
			$killer->increaseHOF(1, ['Killing', 'Kills'], HOF_PUBLIC);
2431
2432
			if ($killer->hasAlliance()) {
2433
				$this->db->write('UPDATE alliance SET alliance_kills=alliance_kills+1 WHERE alliance_id=' . $this->db->escapeNumber($killer->getAllianceID()) . ' AND game_id=' . $this->db->escapeNumber($killer->getGameID()));
2434
			}
2435
2436
			// alliance vs. alliance stats
2437
			$this->incrementAllianceVsDeaths($killer->getAllianceID());
2438
		}
2439
2440
		$dyingHof = ['Dying', 'Players'];
2441
		if ($killer->isNPC()) {
2442
			$dyingHof[] = 'NPC';
2443
		}
2444
		$this->increaseHOF($return['BountyGained']['Amount'], [...$dyingHof, 'Money', 'Bounty Gained By Killer'], HOF_PUBLIC);
2445
		$this->increaseHOF($return['KillerExp'], [...$dyingHof, 'Experience', 'Gained By Killer'], HOF_PUBLIC);
2446
		$this->increaseHOF($return['DeadExp'], [...$dyingHof, 'Experience', 'Lost'], HOF_PUBLIC);
2447
		$this->increaseHOF($return['KillerCredits'], [...$dyingHof, 'Money Lost'], HOF_PUBLIC);
2448
		$this->increaseHOF($this->getShip()->getCost(), [...$dyingHof, 'Money', 'Cost Of Ships Lost'], HOF_PUBLIC);
2449
		$this->increaseHOF(1, [...$dyingHof, 'Deaths'], HOF_PUBLIC);
2450
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2451
2452
		$this->killPlayer($this->getSectorID());
2453
		return $return;
2454
	}
2455
2456
	/**
2457
	 * @return array<string, mixed>
2458
	 */
2459
	public function killPlayerByForces(SmrForce $forces): array {
2460
		$return = [];
2461
		$owner = $forces->getOwner();
2462
		// send a message to the person who died
2463
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2464
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2465
2466
		$news_message = $this->getBBLink();
2467
		if ($this->hasCustomShipName()) {
2468
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2469
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2470
		}
2471
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2472
		// insert the news entry
2473
		$this->db->insert('news', [
2474
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2475
			'time' => $this->db->escapeNumber(Epoch::time()),
2476
			'news_message' => $this->db->escapeString($news_message),
2477
			'killer_id' => $this->db->escapeNumber($owner->getAccountID()),
2478
			'killer_alliance' => $this->db->escapeNumber($owner->getAllianceID()),
2479
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2480
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2481
		]);
2482
2483
		// Player loses 15% experience
2484
		$expLossPercentage = .15;
2485
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2485
		$return['DeadExp'] = /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage);
Loading history...
2486
		$this->decreaseExperience($return['DeadExp']);
2487
2488
		$return['LostCredits'] = $this->getCredits();
2489
2490
		// alliance vs. alliance stats
2491
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2492
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2493
2494
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2495
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Forces', 'Experience Lost'], HOF_PUBLIC);
2496
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Forces', 'Money Lost'], HOF_PUBLIC);
2497
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Forces', 'Cost Of Ships Lost'], HOF_PUBLIC);
2498
		$this->increaseHOF(1, ['Dying', 'Forces', 'Deaths'], HOF_PUBLIC);
2499
2500
		$this->killPlayer($forces->getSectorID());
2501
		return $return;
2502
	}
2503
2504
	/**
2505
	 * @return array<string, mixed>
2506
	 */
2507
	public function killPlayerByPort(AbstractSmrPort $port): array {
2508
		$return = [];
2509
		// send a message to the person who died
2510
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2511
2512
		$news_message = $this->getBBLink();
2513
		if ($this->hasCustomShipName()) {
2514
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2515
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2516
		}
2517
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2518
		// insert the news entry
2519
		$this->db->insert('news', [
2520
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2521
			'time' => $this->db->escapeNumber(Epoch::time()),
2522
			'news_message' => $this->db->escapeString($news_message),
2523
			'killer_id' => $this->db->escapeNumber(ACCOUNT_ID_PORT),
2524
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2525
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2526
		]);
2527
2528
		// Player loses between 15% and 20% experience
2529
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2530
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2530
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2531
		$this->decreaseExperience($return['DeadExp']);
2532
2533
		$return['LostCredits'] = $this->getCredits();
2534
2535
		// alliance vs. alliance stats
2536
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2537
2538
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2539
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Ports', 'Experience Lost'], HOF_PUBLIC);
2540
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Ports', 'Money Lost'], HOF_PUBLIC);
2541
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Ports', 'Cost Of Ships Lost'], HOF_PUBLIC);
2542
		$this->increaseHOF(1, ['Dying', 'Ports', 'Deaths'], HOF_PUBLIC);
2543
2544
		$this->killPlayer($port->getSectorID());
2545
		return $return;
2546
	}
2547
2548
	/**
2549
	 * @return array<string, mixed>
2550
	 */
2551
	public function killPlayerByPlanet(SmrPlanet $planet): array {
2552
		$return = [];
2553
		// send a message to the person who died
2554
		$planetOwner = $planet->getOwner();
2555
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2556
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2557
2558
		$news_message = $this->getBBLink();
2559
		if ($this->hasCustomShipName()) {
2560
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2561
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2562
		}
2563
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2564
		// insert the news entry
2565
		$this->db->insert('news', [
2566
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2567
			'time' => $this->db->escapeNumber(Epoch::time()),
2568
			'news_message' => $this->db->escapeString($news_message),
2569
			'killer_id' => $this->db->escapeNumber($planetOwner->getAccountID()),
2570
			'killer_alliance' => $this->db->escapeNumber($planetOwner->getAllianceID()),
2571
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2572
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2573
		]);
2574
2575
		// Player loses between 15% and 20% experience
2576
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2577
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2577
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2578
		$this->decreaseExperience($return['DeadExp']);
2579
2580
		$return['LostCredits'] = $this->getCredits();
2581
2582
		// alliance vs. alliance stats
2583
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2584
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2585
2586
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2587
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Planets', 'Experience Lost'], HOF_PUBLIC);
2588
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Planets', 'Money Lost'], HOF_PUBLIC);
2589
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Planets', 'Cost Of Ships Lost'], HOF_PUBLIC);
2590
		$this->increaseHOF(1, ['Dying', 'Planets', 'Deaths'], HOF_PUBLIC);
2591
2592
		$this->killPlayer($planet->getSectorID());
2593
		return $return;
2594
	}
2595
2596
	public function incrementAllianceVsKills(int $otherID): void {
2597
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2598
		$this->db->write('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2599
	}
2600
2601
	public function incrementAllianceVsDeaths(int $otherID): void {
2602
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2603
		$this->db->write('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2604
	}
2605
2606
	public function getTurnsLevel(): TurnsLevel {
2607
		return match (true) {
2608
			$this->getTurns() === 0 => TurnsLevel::None,
2609
			$this->getTurns() <= 25 => TurnsLevel::Low,
2610
			$this->getTurns() <= 75 => TurnsLevel::Medium,
2611
			default => TurnsLevel::High,
2612
		};
2613
	}
2614
2615
	public function getTurns(): int {
2616
		return $this->turns;
2617
	}
2618
2619
	public function hasTurns(): bool {
2620
		return $this->turns > 0;
2621
	}
2622
2623
	public function getMaxTurns(): int {
2624
		return $this->getGame()->getMaxTurns();
2625
	}
2626
2627
	public function setTurns(int $turns): void {
2628
		if ($this->turns == $turns) {
2629
			return;
2630
		}
2631
		// Make sure turns are in range [0, MaxTurns]
2632
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2633
		$this->hasChanged = true;
2634
	}
2635
2636
	public function takeTurns(int $take, int $takeNewbie = 0): void {
2637
		if ($take < 0 || $takeNewbie < 0) {
2638
			throw new Exception('Trying to take negative turns.');
2639
		}
2640
		$take = ICeil($take);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2640
		$take = /** @scrutinizer ignore-call */ ICeil($take);
Loading history...
2641
		// Only take up to as many newbie turns as we have remaining
2642
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2643
2644
		$this->setTurns($this->getTurns() - $take);
2645
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2646
		$this->increaseHOF($take, ['Movement', 'Turns Used', 'Since Last Death'], HOF_ALLIANCE);
2647
		$this->increaseHOF($take, ['Movement', 'Turns Used', 'Total'], HOF_ALLIANCE);
2648
		$this->increaseHOF($takeNewbie, ['Movement', 'Turns Used', 'Newbie'], HOF_ALLIANCE);
2649
2650
		// Player has taken an action
2651
		$this->setLastActive(Epoch::time());
2652
		$this->updateLastCPLAction();
2653
	}
2654
2655
	public function giveTurns(int $give, int $giveNewbie = 0): void {
2656
		if ($give < 0 || $giveNewbie < 0) {
2657
			throw new Exception('Trying to give negative turns.');
2658
		}
2659
		$this->setTurns($this->getTurns() + $give);
2660
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2661
	}
2662
2663
	/**
2664
	 * Calculate the time in seconds between the given time and when the
2665
	 * player will be at max turns.
2666
	 */
2667
	public function getTimeUntilMaxTurns(int $time, bool $forceUpdate = false): int {
2668
		$timeDiff = $time - $this->getLastTurnUpdate();
2669
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2670
		$ship = $this->getShip($forceUpdate);
2671
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2671
		$maxTurnsTime = /** @scrutinizer ignore-call */ ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
Loading history...
2672
		// If already at max turns, return 0
2673
		return max(0, $maxTurnsTime);
2674
	}
2675
2676
	/**
2677
	 * Calculate the time in seconds until the next turn is awarded.
2678
	 */
2679
	public function getTimeUntilNextTurn(): int {
2680
		$secondsSinceUpdate = Epoch::time() - $this->getLastTurnUpdate();
2681
		$secondsPerTurn = 3600 / $this->getShip()->getRealSpeed();
2682
		return ICeil(fmod(abs($secondsSinceUpdate - $secondsPerTurn), $secondsPerTurn));
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2682
		return /** @scrutinizer ignore-call */ ICeil(fmod(abs($secondsSinceUpdate - $secondsPerTurn), $secondsPerTurn));
Loading history...
2683
	}
2684
2685
	/**
2686
	 * Grant the player their starting turns.
2687
	 */
2688
	public function giveStartingTurns(): void {
2689
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2689
		$startTurns = /** @scrutinizer ignore-call */ IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
Loading history...
2690
		$this->giveTurns($startTurns);
2691
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2692
	}
2693
2694
	// Turns only update when player is active.
2695
	// Calculate turns gained between given time and the last turn update
2696
	public function getTurnsGained(int $time, bool $forceUpdate = false): int {
2697
		$timeDiff = $time - $this->getLastTurnUpdate();
2698
		$ship = $this->getShip($forceUpdate);
2699
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2699
		$extraTurns = /** @scrutinizer ignore-call */ IFloor($timeDiff * $ship->getRealSpeed() / 3600);
Loading history...
2700
		return $extraTurns;
2701
	}
2702
2703
	public function updateTurns(): void {
2704
		// is account validated?
2705
		if (!$this->getAccount()->isValidated()) {
2706
			return;
2707
		}
2708
2709
		// how many turns would he get right now?
2710
		$extraTurns = $this->getTurnsGained(Epoch::time());
2711
2712
		// do we have at least one turn to give?
2713
		if ($extraTurns > 0) {
2714
			// recalc the time to avoid rounding errors
2715
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2715
			$newLastTurnUpdate = $this->getLastTurnUpdate() + /** @scrutinizer ignore-call */ ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
Loading history...
2716
			$this->setLastTurnUpdate($newLastTurnUpdate);
2717
			$this->giveTurns($extraTurns);
2718
		}
2719
	}
2720
2721
	public function getLastTurnUpdate(): int {
2722
		return $this->lastTurnUpdate;
2723
	}
2724
2725
	public function setLastTurnUpdate(int $time): void {
2726
		if ($this->lastTurnUpdate == $time) {
2727
			return;
2728
		}
2729
		$this->lastTurnUpdate = $time;
2730
		$this->hasChanged = true;
2731
	}
2732
2733
	public function getLastActive(): int {
2734
		return $this->lastActive;
2735
	}
2736
2737
	public function setLastActive(int $lastActive): void {
2738
		if ($this->lastActive == $lastActive) {
2739
			return;
2740
		}
2741
		$this->lastActive = $lastActive;
2742
		$this->hasChanged = true;
2743
	}
2744
2745
	public function getLastCPLAction(): int {
2746
		return $this->lastCPLAction;
2747
	}
2748
2749
	public function setLastCPLAction(int $time): void {
2750
		if ($this->lastCPLAction == $time) {
2751
			return;
2752
		}
2753
		$this->lastCPLAction = $time;
2754
		$this->hasChanged = true;
2755
	}
2756
2757
	public function updateLastCPLAction(): void {
2758
		$this->setLastCPLAction(Epoch::time());
2759
	}
2760
2761
	public function setNewbieWarning(bool $bool): void {
2762
		if ($this->newbieWarning == $bool) {
2763
			return;
2764
		}
2765
		$this->newbieWarning = $bool;
2766
		$this->hasChanged = true;
2767
	}
2768
2769
	public function getNewbieWarning(): bool {
2770
		return $this->newbieWarning;
2771
	}
2772
2773
	public function isDisplayMissions(): bool {
2774
		return $this->displayMissions;
2775
	}
2776
2777
	public function setDisplayMissions(bool $bool): void {
2778
		if ($this->displayMissions == $bool) {
2779
			return;
2780
		}
2781
		$this->displayMissions = $bool;
2782
		$this->hasChanged = true;
2783
	}
2784
2785
	/**
2786
	 * @return array<int, array<string, mixed>>
2787
	 */
2788
	public function getMissions(): array {
2789
		if (!isset($this->missions)) {
2790
			$dbResult = $this->db->read('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2791
			$this->missions = [];
2792
			foreach ($dbResult->records() as $dbRecord) {
2793
				$missionID = $dbRecord->getInt('mission_id');
2794
				$this->missions[$missionID] = [
2795
					'On Step' => $dbRecord->getInt('on_step'),
2796
					'Progress' => $dbRecord->getInt('progress'),
2797
					'Unread' => $dbRecord->getBoolean('unread'),
2798
					'Expires' => $dbRecord->getInt('step_fails'),
2799
					'Sector' => $dbRecord->getInt('mission_sector'),
2800
					'Starting Sector' => $dbRecord->getInt('starting_sector'),
2801
				];
2802
				$this->rebuildMission($missionID);
2803
			}
2804
		}
2805
		return $this->missions;
2806
	}
2807
2808
	/**
2809
	 * @return array<int, array<string, mixed>>
2810
	 */
2811
	public function getActiveMissions(): array {
2812
		$missions = $this->getMissions();
2813
		foreach ($missions as $missionID => $mission) {
2814
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2815
				unset($missions[$missionID]);
2816
			}
2817
		}
2818
		return $missions;
2819
	}
2820
2821
	/**
2822
	 * @return array<string, mixed>|false
2823
	 */
2824
	protected function getMission(int $missionID): array|false {
2825
		$missions = $this->getMissions();
2826
		if (isset($missions[$missionID])) {
2827
			return $missions[$missionID];
2828
		}
2829
		return false;
2830
	}
2831
2832
	protected function hasMission(int $missionID): bool {
2833
		return $this->getMission($missionID) !== false;
2834
	}
2835
2836
	protected function updateMission(int $missionID): bool {
2837
		$this->getMissions();
2838
		if (isset($this->missions[$missionID])) {
2839
			$mission = $this->missions[$missionID];
2840
			$this->db->write('
2841
				UPDATE player_has_mission
2842
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2843
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2844
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2845
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2846
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2847
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2848
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID));
2849
			return true;
2850
		}
2851
		return false;
2852
	}
2853
2854
	private function setupMissionStep(int $missionID): void {
2855
		$stepID = $this->missions[$missionID]['On Step'];
2856
		if ($stepID >= count(MISSIONS[$missionID]['Steps'])) {
2857
			// Nothing to do if this mission is already completed
2858
			return;
2859
		}
2860
		$step = MISSIONS[$missionID]['Steps'][$stepID];
2861
		if (isset($step['PickSector'])) {
2862
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2863
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2864
			if ($path === false) {
2865
				// Abandon the mission if it cannot be completed due to a
2866
				// sector that does not exist or cannot be reached.
2867
				// (Probably shouldn't bestow this mission in the first place)
2868
				$this->deleteMission($missionID);
2869
				throw new UserError('Cannot find a path to the destination!');
2870
			}
2871
			$this->missions[$missionID]['Sector'] = $path->getEndSectorID();
2872
		}
2873
	}
2874
2875
	/**
2876
	 * Declining a mission will permanently hide it from the player
2877
	 * by adding it in its completed state.
2878
	 */
2879
	public function declineMission(int $missionID): void {
2880
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2881
		$this->addMission($missionID, $finishedStep);
2882
	}
2883
2884
	public function addMission(int $missionID, int $step = 0): void {
2885
		if ($this->hasMission($missionID)) {
2886
			throw new Exception('Mission ID already added: ' . $missionID);
2887
		}
2888
2889
		$mission = [
2890
			'On Step' => $step,
2891
			'Progress' => 0,
2892
			'Unread' => true,
2893
			'Expires' => (Epoch::time() + 86400),
2894
			'Sector' => 0,
2895
			'Starting Sector' => $this->getSectorID(),
2896
		];
2897
2898
		$this->missions[$missionID] =& $mission;
2899
		$this->setupMissionStep($missionID);
2900
		$this->rebuildMission($missionID);
2901
2902
		$this->db->replace('player_has_mission', [
2903
			'game_id' => $this->db->escapeNumber($this->gameID),
2904
			'account_id' => $this->db->escapeNumber($this->accountID),
2905
			'mission_id' => $this->db->escapeNumber($missionID),
2906
			'on_step' => $this->db->escapeNumber($mission['On Step']),
2907
			'progress' => $this->db->escapeNumber($mission['Progress']),
2908
			'unread' => $this->db->escapeBoolean($mission['Unread']),
2909
			'starting_sector' => $this->db->escapeNumber($mission['Starting Sector']),
2910
			'mission_sector' => $this->db->escapeNumber($mission['Sector']),
2911
			'step_fails' => $this->db->escapeNumber($mission['Expires']),
2912
		]);
2913
	}
2914
2915
	private function rebuildMission(int $missionID): void {
2916
		$mission = $this->missions[$missionID];
2917
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2918
2919
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2920
			// If we have completed this mission just use false to indicate no current task.
2921
			$currentStep = false;
2922
		} else {
2923
			$data = ['player' => $this, 'mission' => $mission];
2924
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2925
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2926
		}
2927
		$this->missions[$missionID]['Task'] = $currentStep;
2928
	}
2929
2930
	public function deleteMission(int $missionID): void {
2931
		$this->getMissions();
2932
		if (isset($this->missions[$missionID])) {
2933
			unset($this->missions[$missionID]);
2934
			$this->db->write('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID));
2935
			return;
2936
		}
2937
		throw new Exception('Mission with ID not found: ' . $missionID);
2938
	}
2939
2940
	/**
2941
	 * @return array<int>
2942
	 */
2943
	public function markMissionsRead(): array {
2944
		$this->getMissions();
2945
		$unreadMissions = [];
2946
		foreach ($this->missions as $missionID => &$mission) {
2947
			if ($mission['Unread']) {
2948
				$unreadMissions[] = $missionID;
2949
				$mission['Unread'] = false;
2950
				$this->updateMission($missionID);
2951
			}
2952
		}
2953
		return $unreadMissions;
2954
	}
2955
2956
	public function claimMissionReward(int $missionID): string {
2957
		if (!$this->hasMission($missionID)) {
2958
			throw new Exception('Unknown mission: ' . $missionID);
2959
		}
2960
		$mission =& $this->missions[$missionID];
2961
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2962
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2963
		}
2964
		$mission['On Step']++;
2965
		$mission['Unread'] = true;
2966
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2967
			switch ($rewardItem) {
2968
				case 'Credits':
2969
					$this->increaseCredits($amount);
2970
					break;
2971
				case 'Experience':
2972
					$this->increaseExperience($amount);
2973
					break;
2974
			}
2975
		}
2976
		$rewardText = $mission['Task']['Rewards']['Text'];
2977
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2978
			// If we haven't finished the mission yet then
2979
			$this->setupMissionStep($missionID);
2980
		}
2981
		$this->rebuildMission($missionID);
2982
		$this->updateMission($missionID);
2983
		return $rewardText;
2984
	}
2985
2986
	/**
2987
	 * @return array<int, array<string, mixed>>
2988
	 */
2989
	public function getAvailableMissions(): array {
2990
		$availableMissions = [];
2991
		foreach (MISSIONS as $missionID => $mission) {
2992
			if ($this->hasMission($missionID)) {
2993
				continue;
2994
			}
2995
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2996
			if ($this->getSector()->hasX($realX)) {
2997
				$availableMissions[$missionID] = $mission;
2998
			}
2999
		}
3000
		return $availableMissions;
3001
	}
3002
3003
	/**
3004
	 * Log a player action in the current sector to the admin log console.
3005
	 */
3006
	public function log(int $log_type_id, string $msg): void {
3007
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
3008
	}
3009
3010
	/**
3011
	 * @param array<string, mixed> $values
3012
	 */
3013
	public function actionTaken(string $actionID, array $values): void {
3014
		if (!in_array($actionID, MISSION_ACTIONS)) {
3015
			throw new Exception('Unknown action: ' . $actionID);
3016
		}
3017
		// TODO: Reenable this once tested.     if($this->getAccount()->isLoggingEnabled())
3018
		switch ($actionID) {
3019
			case 'WalkSector':
3020
				$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
3021
				break;
3022
			case 'JoinAlliance':
3023
				$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
3024
				break;
3025
			case 'LeaveAlliance':
3026
				$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
3027
				break;
3028
			case 'DisbandAlliance':
3029
				$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
3030
				break;
3031
			case 'KickPlayer':
3032
				$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
3033
				break;
3034
			case 'PlayerKicked':
3035
				$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
3036
				break;
3037
		}
3038
3039
		$this->getMissions();
3040
		foreach ($this->missions as $missionID => &$mission) {
3041
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
3042
				$requirements = $mission['Task']['Detail'];
3043
				if (checkMissionRequirements($values, $requirements) === true) {
3044
					$mission['On Step']++;
3045
					$mission['Unread'] = true;
3046
					$this->setupMissionStep($missionID);
3047
					$this->rebuildMission($missionID);
3048
					$this->updateMission($missionID);
3049
				}
3050
			}
3051
		}
3052
	}
3053
3054
	/**
3055
	 * @param array<SmrPlayer> $otherPlayerArray
3056
	 */
3057
	public function canSeeAny(array $otherPlayerArray): bool {
3058
		foreach ($otherPlayerArray as $otherPlayer) {
3059
			if ($this->canSee($otherPlayer)) {
3060
				return true;
3061
			}
3062
		}
3063
		return false;
3064
	}
3065
3066
	public function canSee(self $otherPlayer): bool {
3067
		if (!$otherPlayer->getShip()->isCloaked()) {
3068
			return true;
3069
		}
3070
		if ($this->sameAlliance($otherPlayer)) {
3071
			return true;
3072
		}
3073
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
3074
			return true;
3075
		}
3076
		return false;
3077
	}
3078
3079
	public function equals(self $otherPlayer = null): bool {
3080
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
3081
	}
3082
3083
	public function sameAlliance(self $otherPlayer = null): bool {
3084
		return $this->equals($otherPlayer) || ($otherPlayer !== null && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
3085
	}
3086
3087
	public function sharedForceAlliance(self $otherPlayer = null): bool {
3088
		return $this->sameAlliance($otherPlayer);
3089
	}
3090
3091
	public function forceNAPAlliance(self $otherPlayer = null): bool {
3092
		return $this->sameAlliance($otherPlayer);
3093
	}
3094
3095
	public function planetNAPAlliance(self $otherPlayer = null): bool {
3096
		return $this->sameAlliance($otherPlayer);
3097
	}
3098
3099
	public function traderNAPAlliance(self $otherPlayer = null): bool {
3100
		return $this->sameAlliance($otherPlayer);
3101
	}
3102
3103
	public function traderMAPAlliance(self $otherPlayer = null): bool {
3104
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
3105
	}
3106
3107
	public function traderAttackTraderAlliance(self $otherPlayer = null): bool {
3108
		return $this->sameAlliance($otherPlayer);
3109
	}
3110
3111
	public function traderDefendTraderAlliance(self $otherPlayer = null): bool {
3112
		return $this->sameAlliance($otherPlayer);
3113
	}
3114
3115
	public function traderAttackForceAlliance(self $otherPlayer = null): bool {
3116
		return $this->sameAlliance($otherPlayer);
3117
	}
3118
3119
	public function traderAttackPortAlliance(self $otherPlayer = null): bool {
3120
		return $this->sameAlliance($otherPlayer);
3121
	}
3122
3123
	public function traderAttackPlanetAlliance(self $otherPlayer = null): bool {
3124
		return $this->sameAlliance($otherPlayer);
3125
	}
3126
3127
	public function meetsAlignmentRestriction(int $restriction): bool {
3128
		if ($restriction < 0) {
3129
			return $this->getAlignment() <= $restriction;
3130
		}
3131
		if ($restriction > 0) {
3132
			return $this->getAlignment() >= $restriction;
3133
		}
3134
		return true;
3135
	}
3136
3137
	/**
3138
	 * Get an array of goods that are visible to the player
3139
	 *
3140
	 * @return array<int, array<string, string|int>>
3141
	 */
3142
	public function getVisibleGoods(): array {
3143
		$goods = Globals::getGoods();
3144
		$visibleGoods = [];
3145
		foreach ($goods as $key => $good) {
3146
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
3147
				$visibleGoods[$key] = $good;
3148
			}
3149
		}
3150
		return $visibleGoods;
3151
	}
3152
3153
	/**
3154
	 * Returns an array of all unvisited sectors.
3155
	 *
3156
	 * @return array<int>
3157
	 */
3158
	public function getUnvisitedSectors(): array {
3159
		if (!isset($this->unvisitedSectors)) {
3160
			$this->unvisitedSectors = [];
3161
			// Note that this table actually has entries for the *unvisited* sectors!
3162
			$dbResult = $this->db->read('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3163
			foreach ($dbResult->records() as $dbRecord) {
3164
				$this->unvisitedSectors[] = $dbRecord->getInt('sector_id');
3165
			}
3166
		}
3167
		return $this->unvisitedSectors;
3168
	}
3169
3170
	/**
3171
	 * Check if player has visited the input sector.
3172
	 * Note that this populates the list of *all* unvisited sectors!
3173
	 */
3174
	public function hasVisitedSector(int $sectorID): bool {
3175
		return !in_array($sectorID, $this->getUnvisitedSectors());
3176
	}
3177
3178
	public function getLeaveNewbieProtectionHREF(): string {
3179
		return (new NewbieLeaveProcessor())->href();
3180
	}
3181
3182
	public function getExamineTraderHREF(): string {
3183
		$container = new ExamineTrader($this->getAccountID());
3184
		return $container->href();
3185
	}
3186
3187
	public function getAttackTraderHREF(): string {
3188
		return Globals::getAttackTraderHREF($this->getAccountID());
3189
	}
3190
3191
	public function getPlanetKickHREF(): string {
3192
		$container = new KickProcessor($this->getAccountID());
3193
		return $container->href();
3194
	}
3195
3196
	public function getTraderSearchHREF(): string {
3197
		$container = new SearchForTraderResult($this->getPlayerID());
3198
		return $container->href();
3199
	}
3200
3201
	public function getAllianceRosterHREF(): string {
3202
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3203
	}
3204
3205
	public function getToggleWeaponHidingHREF(bool $ajax = false): string {
3206
		$container = new WeaponDisplayToggleProcessor();
3207
		$container->allowAjax = $ajax;
3208
		return $container->href();
3209
	}
3210
3211
	public function isDisplayWeapons(): bool {
3212
		return $this->displayWeapons;
3213
	}
3214
3215
	/**
3216
	 * Should weapons be displayed in the right panel?
3217
	 * This updates the player database directly because it is used with AJAX,
3218
	 * which does not acquire a sector lock.
3219
	 */
3220
	public function setDisplayWeapons(bool $bool): void {
3221
		if ($this->displayWeapons == $bool) {
3222
			return;
3223
		}
3224
		$this->displayWeapons = $bool;
3225
		$this->db->write('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3226
	}
3227
3228
	public function update(): void {
3229
		$this->save();
3230
	}
3231
3232
	public function save(): void {
3233
		if ($this->hasChanged === true) {
3234
			$this->db->write('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3235
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3236
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3237
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3238
				', turns=' . $this->db->escapeNumber($this->turns) .
3239
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3240
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3241
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3242
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3243
				', dead=' . $this->db->escapeBoolean($this->dead) .
3244
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3245
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3246
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3247
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3248
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3249
				', credits=' . $this->db->escapeNumber($this->credits) .
3250
				', experience=' . $this->db->escapeNumber($this->experience) .
3251
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3252
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3253
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3254
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3255
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3256
				', kills=' . $this->db->escapeNumber($this->kills) .
3257
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3258
				', assists=' . $this->db->escapeNumber($this->assists) .
3259
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3260
				', bank=' . $this->db->escapeNumber($this->bank) .
3261
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3262
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3263
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3264
				', group_scout_messages=' . $this->db->escapeString($this->scoutMessageGroupType->value) .
3265
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3266
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3267
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3268
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3269
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3270
				', under_attack = ' . $this->db->escapeBoolean($this->underAttack) .
3271
				' WHERE ' . $this->SQL);
3272
			$this->hasChanged = false;
3273
		}
3274
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3275
			if ($bountyChanged === true) {
3276
				$bountyChanged = false;
3277
				$bounty = $this->getBounty($key);
3278
				if ($bounty['New'] === true) {
3279
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3280
						$this->db->insert('bounty', [
3281
							'account_id' => $this->db->escapeNumber($this->getAccountID()),
3282
							'game_id' => $this->db->escapeNumber($this->getGameID()),
3283
							'type' => $this->db->escapeString($bounty['Type']->value),
3284
							'amount' => $this->db->escapeNumber($bounty['Amount']),
3285
							'smr_credits' => $this->db->escapeNumber($bounty['SmrCredits']),
3286
							'claimer_id' => $this->db->escapeNumber($bounty['Claimer']),
3287
							'time' => $this->db->escapeNumber($bounty['Time']),
3288
						]);
3289
					}
3290
				} else {
3291
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3292
						$this->db->write('UPDATE bounty
3293
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3294
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3295
							type=' . $this->db->escapeString($bounty['Type']->value) . ',
3296
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3297
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3298
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL);
3299
					} else {
3300
						$this->db->write('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL);
3301
					}
3302
				}
3303
			}
3304
		}
3305
		$this->saveHOF();
3306
	}
3307
3308
	public function saveHOF(): void {
3309
		foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3310
			if ($changeType == self::HOF_NEW) {
3311
				$this->db->insert('hof_visibility', [
3312
					'type' => $this->db->escapeString($hofType),
3313
					'visibility' => $this->db->escapeString(self::$HOFVis[$hofType]),
3314
				]);
3315
			} else {
3316
				$this->db->write('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType));
3317
			}
3318
			unset(self::$hasHOFVisChanged[$hofType]);
3319
		}
3320
3321
		foreach ($this->hasHOFChanged as $hofType => $changeType) {
3322
			$amount = $this->HOF[$hofType];
3323
			if ($changeType === self::HOF_NEW) {
3324
				if ($amount > 0) {
3325
					$this->db->insert('player_hof', [
3326
						'account_id' => $this->db->escapeNumber($this->getAccountID()),
3327
						'game_id' => $this->db->escapeNumber($this->getGameID()),
3328
						'type' => $this->db->escapeString($hofType),
3329
						'amount' => $this->db->escapeNumber($amount),
3330
					]);
3331
				}
3332
			} elseif ($changeType === self::HOF_CHANGED) {
3333
				$this->db->write('UPDATE player_hof
3334
					SET amount=' . $this->db->escapeNumber($amount) . '
3335
					WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeString($hofType));
3336
			}
3337
			unset($this->hasHOFChanged[$hofType]);
3338
		}
3339
	}
3340
3341
}
3342